From 30bcbedf5efeeb186d3079f2082795df560edaf8 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 20 Jan 2026 17:21:44 -0800 Subject: [PATCH 01/40] 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 1cb66ab5..101828b3 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 5b804eb5..74e5cf98 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 02/40] 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 09039f07..cefeb2a9 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 03/40] 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 43cacc99..b8dd623d 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 b4e35edb..92ebc463 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 00000000..fa8e5ffd --- /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 1b5e5de3..e5f40f38 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 07f395a6..0bb17c84 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 1c1ee514..56cb1cc1 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 b406e942..4b6b5262 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 785ee377..1b8d35d1 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 b0c13a00..cd031fbf 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 fe4fc016..d52830ca 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 314e702f..9b70d9c4 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 b884f3c1..9bab237e 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 96ba1b97..86f18ba2 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 029ed11e..908c88c8 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 1dce57f6..48a6ac48 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 20df619a..83010d8d 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 50f5744b..ce60a8f8 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 378858a0..13fbeb0a 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 ae02aff3..3f54001f 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 81bf16aa..75d46549 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 cdcd23c7..be9556f3 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 8b36b976..34b54b46 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 3d7bd06e..cd9c3be8 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 75a6cc09..48ef3ddd 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 50732d10..823bba1e 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 34797bea..b48db37b 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 d6ea7d83..71871d16 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 09ee0bcf..977f29cd 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 4d302a56..f1da7c8b 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 c0704140..ae10f604 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 46648433..84aeb0a6 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 1cb66ab5..a120a0d8 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 5764bc88..a1707b95 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 e71a16d2..f6f23bfe 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 2e1de87a..f16b4c34 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 fdb93ad1..31428df1 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 de12f546..2e2b7466 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 490c83d6..c9e9120b 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 2138a62f..9e26dfee 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 c158b14b..eeb548fa 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 905321af..68825d8b 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 04/40] 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 8469d615..f9241e88 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 05/40] 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 69ed111d..1cecf976 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 84aeb0a6..380c7ce6 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 a1707b95..079f25d1 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 06/40] 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 a120a0d8..30a99f06 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 07/40] 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 1b8d35d1..f3015ccf 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 48a6ac48..105997da 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 08/40] 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 f3015ccf..e50fb824 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 09/40] 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 105997da..74587b2e 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 10/40] 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 e50fb824..8abb5365 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 74587b2e..918cee65 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 11/40] 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 8abb5365..43cf4ece 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 12/40] 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 00000000..a58d554b --- /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 00000000..492805ec --- /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 13/40] 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 f9241e88..a4faf0e8 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 e91cd943..02faff58 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 a53ad5d9..07ac6899 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 14/40] 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 00000000..b1e6c1f2 --- /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 00000000..7ac1e85f --- /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 15/40] 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 43cf4ece..1b695405 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 16/40] 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 918cee65..07ec4c89 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 17/40] 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 00000000..22201338 --- /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 29f92af9..28b10821 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 00000000..d2c73fbf --- /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 380c7ce6..f45ed34b 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 079f25d1..efc35378 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 402565f1..b98bdc65 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 00000000..08614f35 --- /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 18/40] 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 28b10821..0d5b4b15 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 e91cd943..54f819c6 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 19/40] 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 d52830ca..5e2d8fee 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 b1e6c1f2..ae784e49 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 7ac1e85f..e0c2cbe0 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 20/40] 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 5e2d8fee..d40c7918 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 a58d554b..bc431eae 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 21/40] 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 c9e9120b..56556e4c 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 22/40] 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 592ceee8..ec97fc6f 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 592ceee8..c4855bfe 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 00000000..5f672923 --- /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 0d5b4b15..de39d7e4 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 c2efd0b6..4b81f9b2 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 c2efd0b6..5caa9d15 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 00000000..ff5ddb3b --- /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 00000000..a87b4cf7 --- /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 6a856469..defe6932 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 1d526a16..21a3cc14 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 23/40] 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 5f672923..aef2502a 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 09c8350f..4d5727ac 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 1d526a16..21a3cc14 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 24/40] 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 56556e4c..8b1415f6 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 25/40] 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 4d5727ac..48d9fff2 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 26/40] 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 be9556f3..38bfe3d9 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 977f29cd..346cdaa6 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 27/40] 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 0bb17c84..3aba6e50 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 9bab237e..53bec5e5 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 28/40] 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 a4faf0e8..f987a62b 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 02faff58..01e777ab 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 00000000..c5e4cadd --- /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 29/40] 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 cefeb2a9..1d25f921 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 30/40] 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 364defff..831c1ecb 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 31/40] 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 56cb1cc1..d191370a 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 01e777ab..d3fcaa02 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 5a545f37..846d0c5d 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 c5e4cadd..2ffb110e 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 32/40] 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 2ffb110e..0e6fd19d 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 838ecf8c..69f8dc66 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 33/40] 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 f987a62b..470b795d 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 831c1ecb..c9e40ab7 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 d3fcaa02..efaacc66 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 0e6fd19d..958258bc 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 34/40] 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 846d0c5d..903f89e6 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 54c01504..1f767f6c 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 69f8dc66..1d2554df 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 34a6b5d895f46d5b6c02345beabdefd6cbb29bda Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 28 Jan 2026 19:55:08 -0800 Subject: [PATCH 35/40] 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 de39d7e4..c378bff3 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 63f4a3c0..642ce1fb 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 36/40] 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 c378bff3..191f74ea 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 642ce1fb..0d38d98b 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 37/40] 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 191f74ea..6f22c5e2 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 38/40] 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 e5f40f38..958d9633 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 0bb17c84..8e45b7c1 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 4b6b5262..1cdfb7bc 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 1b695405..88c65d61 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 cd031fbf..acd440b8 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 d40c7918..ec047bdd 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 9b70d9c4..520b00d4 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 9bab237e..e0ccd3db 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 86f18ba2..9a1634ff 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 908c88c8..7f2f4898 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 07ec4c89..fbc797f3 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 83010d8d..b5b100a6 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 ce60a8f8..3ca198c3 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 13fbeb0a..491f76dd 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 3f54001f..f88a4970 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 ae784e49..72da35c9 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 75d46549..23e3f1af 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 38bfe3d9..4ad59e87 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 34b54b46..885d7d67 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 bc431eae..9f223da3 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 cd9c3be8..fc8d78ba 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 48ef3ddd..b28d668a 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 823bba1e..8070ac31 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 b48db37b..6994bea0 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 e0c2cbe0..f007aa7c 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 71871d16..4e66af0a 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 346cdaa6..805621b6 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 f1da7c8b..da017bed 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 492805ec..85ce4a26 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 ae10f604..5f0c7977 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 06a95f29..84d172a8 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 39/40] 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 d191370a..cb7b95e4 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 ec047bdd..ac3eb99e 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 520b00d4..27b20075 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 905792f8..69e6a59d 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 9a1634ff..a609dd81 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 7f2f4898..28d3e9dc 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 fbc797f3..ce6f6a9d 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 b5b100a6..a7ac6a6a 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 3ca198c3..b55dc414 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 491f76dd..0f7a7040 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 f88a4970..5c252760 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 72da35c9..a944fab4 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 23e3f1af..02f2b620 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 4ad59e87..21d7b6fc 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 885d7d67..a96d7dcb 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 9f223da3..6107c5b5 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 fc8d78ba..c10a7458 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 377c39a2..ce612313 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 9e26dfee..b9dadf3e 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 40/40] 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 6f22c5e2..1bab1301 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 004904bb..b75a9fd5 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 f45ed34b..083a60b7 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 efc35378..cf343818 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 1a2ffa31..7345eff2 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 31428df1..b4a41dd1 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 2e2b7466..1e275d4f 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 00000000..d2c70b5b --- /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)); + }); + }); + }); +}