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/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/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 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/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 00000000..aef2502a
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,152 @@
+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
+ - url_launcher_ios (0.0.1):
+ - Flutter
+ - 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`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+ - 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"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+ 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
+ url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
+ wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
+
+PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
+
+COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 09c8350f..48d9fff2 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 */
@@ -368,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;
@@ -384,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";
@@ -401,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";
@@ -416,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";
@@ -547,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";
@@ -569,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;
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 @@
+
+
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/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart
index 29f92af9..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;
@@ -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();
}
@@ -620,6 +625,32 @@ 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, 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));
+ }
+
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults.clear();
for (var result in results) {
@@ -928,7 +959,12 @@ class MeshCoreConnector extends ChangeNotifier {
if (!isConnected) return;
if (_batteryRequested && !force) return;
_batteryRequested = true;
- await sendFrame(buildGetBattAndStorageFrame());
+ try {
+ await sendFrame(buildGetBattAndStorageFrame());
+ } catch (e) {
+ // Connection likely lost - trigger disconnection handling
+ _handleDisconnection();
+ }
}
void _startBatteryPolling() {
@@ -1252,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;
}
@@ -1274,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();
@@ -2652,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();
}
@@ -2686,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;
}
}
@@ -2845,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
@@ -2865,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
}
@@ -2963,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/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart
index 8469d615..470b795d 100644
--- a/lib/connector/meshcore_protocol.dart
+++ b/lib/connector/meshcore_protocol.dart
@@ -18,6 +18,10 @@ class BufferReader {
return data;
}
+ void skipBytes(int count) {
+ _pointer += count;
+ }
+
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
@@ -127,6 +131,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 +181,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;
@@ -195,8 +201,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;
@@ -708,3 +714,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/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/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/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/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index 1b5e5de3..958d9633 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,59 @@
"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}\"",
+ "@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 07f395a6..2586877c 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": {
@@ -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.",
@@ -1014,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",
@@ -1349,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.",
@@ -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,59 @@
"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": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich",
+ "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
+ "community_regenerateSecret": "Neugenerierung des Schlüssels",
+ "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert",
+ "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
+ "community_updateSecret": "Aktualisieren Sie den Schlüssel",
+ "@contacts_pathTraceTo": {
+ "placeholders": {
+ "name": {
+ "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_en.arb b/lib/l10n/app_en.arb
index 1c1ee514..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",
@@ -550,6 +552,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",
@@ -1298,5 +1310,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/l10n/app_es.arb b/lib/l10n/app_es.arb
index b406e942..1cdfb7bc 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,59 @@
"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",
+ "@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 785ee377..88c65d61 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",
@@ -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": {
@@ -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.",
@@ -687,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": {
@@ -734,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": {
@@ -751,7 +763,7 @@
}
}
},
- "time_justNow": "Il y a tout juste maintenant",
+ "time_justNow": "Maintenant",
"time_minutesAgo": "{minutes} minutes auparavant",
"@time_minutesAgo": {
"placeholders": {
@@ -899,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": {
@@ -1012,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 ?",
@@ -1108,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.",
@@ -1327,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": "Room servers",
"listFilter_unreadOnly": "Messages non lus seulement",
- "listFilter_newGroup": "Nouvelle groupe",
+ "listFilter_newGroup": "Nouveau groupe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
@@ -1362,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": {
@@ -1379,11 +1391,11 @@
},
"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_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",
+ "settings_locationGPSEnable": "Activer le GPS",
+ "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS",
+ "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
+ "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
+ "contacts_manageRoom": "Gérer le Room Server",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
@@ -1473,16 +1485,72 @@
"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",
"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)",
"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}\"",
+ "@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 b0c13a00..acd440b8 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,59 @@
"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}\"",
+ "@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 fe4fc016..ac3eb99e 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -14,9 +14,11 @@ 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';
+import 'app_localizations_uk.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
@@ -114,9 +116,11 @@ abstract class AppLocalizations {
Locale('nl'),
Locale('pl'),
Locale('pt'),
+ Locale('ru'),
Locale('sk'),
Locale('sl'),
Locale('sv'),
+ Locale('uk'),
Locale('zh'),
];
@@ -942,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:
@@ -2226,6 +2242,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:
@@ -4653,6 +4699,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
@@ -4675,9 +4793,11 @@ class _AppLocalizationsDelegate
'nl',
'pl',
'pt',
+ 'ru',
'sk',
'sl',
'sv',
+ 'uk',
'zh',
].contains(locale.languageCode);
@@ -4706,12 +4826,16 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsPl();
case 'pt':
return AppLocalizationsPt();
+ case 'ru':
+ return AppLocalizationsRu();
case 'sk':
return AppLocalizationsSk();
case 'sl':
return AppLocalizationsSl();
case 'sv':
return AppLocalizationsSv();
+ case 'uk':
+ return AppLocalizationsUk();
case 'zh':
return AppLocalizationsZh();
}
diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart
index 314e702f..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 => 'Уведомления';
@@ -1207,6 +1213,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 +2591,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
@@ -2658,4 +2682,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 b884f3c1..69e6a59d 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';
@@ -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';
@@ -662,7 +668,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 +802,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 +1035,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
@@ -1206,6 +1212,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';
@@ -1870,8 +1894,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 =>
@@ -2339,7 +2362,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.';
@@ -2570,32 +2593,32 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Neugenerierung des Schlüssels';
@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 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Aktualisieren Sie den Schlüssel';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Schlüssel 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
@@ -2663,4 +2686,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 96ba1b97..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';
@@ -1186,6 +1192,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';
@@ -2618,4 +2642,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 029ed11e..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';
@@ -1204,6 +1210,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 +2589,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
@@ -2657,4 +2681,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 1dce57f6..ce6f6a9d 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -204,18 +204,19 @@ 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)';
+ 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';
@@ -272,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';
@@ -447,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';
@@ -667,7 +674,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 +694,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 +718,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 +852,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 +895,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 +943,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String chat_retryCount(int current, int max) {
- return 'Réessayer $current/$max';
+ return 'Essai $current/$max';
}
@override
@@ -1209,6 +1216,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';
@@ -1371,7 +1396,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
@@ -1425,11 +1450,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) {
@@ -1725,7 +1750,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(
@@ -1873,7 +1898,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';
@@ -2070,7 +2095,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 =>
@@ -2581,32 +2606,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
@@ -2614,7 +2639,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é';
@@ -2645,7 +2670,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';
@@ -2666,11 +2691,50 @@ class AppLocalizationsFr extends AppLocalizations {
String get listFilter_repeaters => 'Répéteurs';
@override
- String get listFilter_roomServers => 'Serveurs de pièce';
+ String get listFilter_roomServers => 'Room servers';
@override
String get listFilter_unreadOnly => 'Messages non lus seulement';
@override
- String get listFilter_newGroup => 'Nouvelle groupe';
+ 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 20df619a..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';
@@ -1203,6 +1209,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 +2589,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
@@ -2657,4 +2681,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 50f5744b..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';
@@ -1199,6 +1205,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 +2580,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
@@ -2648,4 +2672,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 378858a0..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';
@@ -1205,6 +1211,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 +2588,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
@@ -2656,4 +2680,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 ae02aff3..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';
@@ -1204,6 +1210,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 +2591,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
@@ -2659,4 +2683,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
new file mode 100644
index 00000000..a944fab4
--- /dev/null
+++ b/lib/l10n/app_localizations_ru.dart
@@ -0,0 +1,2726 @@
+// 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 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 => 'Введите широту и долготу.';
+
+ @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_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 =>
+ 'Циклически переключаться между лучшими маршрутами и режимом рассылки';
+
+ @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: 'хопов',
+ 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-координаты, чтобы отображаться на карте';
+
+ @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: 'хопов',
+ 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 =>
+ 'Авто (использовать сохранённый маршрут)';
+
+ @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 =>
+ 'Показывает конфигурацию передачи местоположения в анонсированиях:\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В';
+ }
+
+ @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 => 'Новая группа';
+
+ @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 81bf16aa..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';
@@ -1200,6 +1206,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 +2577,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
@@ -2644,4 +2668,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 cdcd23c7..21d7b6fc 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';
@@ -144,7 +144,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_scan => 'Skeniraj';
@override
- String get device_quickSwitch => 'Hitro preklopiti';
+ String get device_quickSwitch => 'Hitro preklop';
@override
String get device_meshcore => 'MeshCore';
@@ -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,7 +201,7 @@ 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';
@@ -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,10 +234,10 @@ 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';
@@ -253,47 +253,46 @@ 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';
@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';
+ String get settings_rebootDeviceSubtitle => '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';
@@ -304,11 +303,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 +322,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 +349,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';
@@ -368,13 +367,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 => 'DDolg doseg';
@override
- String get settings_fastSpeed => 'Hitra hitrost';
+ String get settings_fastSpeed => 'Visoka hitrost';
@override
String settings_error(String message) {
@@ -391,10 +390,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 +444,16 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_languageBg => 'Български';
@override
- String get appSettings_notifications => 'Obveščanja';
+ String get appSettings_languageRu => 'Русский';
@override
- String get appSettings_enableNotifications => 'Omogoči obveščanje';
+ String get appSettings_languageUk => 'Українська';
+
+ @override
+ String get appSettings_notifications => 'Obvestila';
+
+ @override
+ String get appSettings_enableNotifications => 'Omogoči obvestila';
@override
String get appSettings_enableNotificationsSubtitle =>
@@ -484,7 +489,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 +504,19 @@ 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 +530,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 +551,51 @@ 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';
+ String get appSettings_showRepeatersSubtitle => '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 +604,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 +618,78 @@ 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, 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.';
+ String get contacts_noContactsFound => '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 => 'Upravljaj Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik 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 +700,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 +757,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 +776,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
@@ -912,21 +916,21 @@ 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) {
@@ -937,10 +941,10 @@ class AppLocalizationsSl extends AppLocalizations {
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 +965,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 +986,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 ustvarjenih 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) {
@@ -1079,10 +1083,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 +1107,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 +1119,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 +1137,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 =>
@@ -1197,6 +1201,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';
@@ -1994,13 +2016,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';
@@ -2012,7 +2034,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';
@@ -2135,7 +2157,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 =>
@@ -2243,11 +2265,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 =>
@@ -2362,13 +2384,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) {
@@ -2533,17 +2555,17 @@ 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';
@override
String community_deleteConfirm(String name) {
- return 'Zapustiti \"$name\"?';
+ return 'Zapusti \"$name\"?';
}
@override
@@ -2557,36 +2579,36 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Ponovno ustvari geslo';
@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 'Geslo za \"$name\" ponovno ustvarjeno';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Ažuriraj ključ';
@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 novo QR kodo za posodabljanje ključa za $name';
}
@override
- String get community_addHashtagChannel => 'Dodaj Oznako Obštnine';
+ String get community_addHashtagChannel => 'Dodaj hashtag kanal';
@override
String get community_addHashtagChannelDesc =>
@@ -2600,7 +2622,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get community_regularHashtagDesc =>
- 'javna oznaka (kateri koli lahko sodelujejo)';
+ 'javna oznaka (kdorkoli lahko sodeluje)';
@override
String get community_communityHashtag => 'Skupnostni hashtag';
@@ -2649,4 +2671,42 @@ class AppLocalizationsSl extends AppLocalizations {
@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 8b36b976..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';
@@ -1192,6 +1198,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 +2565,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
@@ -2632,4 +2656,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
new file mode 100644
index 00000000..6107c5b5
--- /dev/null
+++ b/lib/l10n/app_localizations_uk.dart
@@ -0,0 +1,2733 @@
+// 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_languageRu => 'Русский';
+
+ @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 => 'Нова група';
+
+ @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 3d7bd06e..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 => '通知';
@@ -1148,6 +1154,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 +2448,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
@@ -2514,4 +2537,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 75a6cc09..b28d668a 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,59 @@
"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",
+ "@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 50732d10..8070ac31 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,59 @@
"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",
+ "@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 34797bea..6994bea0 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,59 @@
"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",
+ "@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
new file mode 100644
index 00000000..f007aa7c
--- /dev/null
+++ b/lib/l10n/app_ru.arb
@@ -0,0 +1,797 @@
+{
+ "@@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} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
+ "chat_successes": "успешно",
+ "chat_removePath": "Удалить маршрут",
+ "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.",
+ "chat_pathActions": "Действия с маршрутом:",
+ "chat_setCustomPath": "Указать маршрут вручную",
+ "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи",
+ "chat_clearPath": "Очистить маршрут",
+ "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке",
+ "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.",
+ "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения",
+ "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.",
+ "chat_fullPath": "Полный маршрут",
+ "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.",
+ "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}",
+ "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.",
+ "chat_pathDeviceConfirmed": "Подтверждено устройством.",
+ "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.",
+ "chat_type": "Тип",
+ "chat_path": "Маршрут",
+ "chat_publicKey": "Публичный ключ",
+ "chat_compressOutgoingMessages": "Сжимать исходящие сообщения",
+ "chat_floodForced": "Рассылка (принудительно)",
+ "chat_directForced": "Прямой (принудительно)",
+ "chat_hopsForced": "{count} хоп(ов) (принудительно)",
+ "chat_floodAuto": "Рассылка (авто)",
+ "chat_direct": "Прямой",
+ "chat_poiShared": "Точка интереса отправлена",
+ "chat_unread": "Непрочитанных: {count}",
+ "map_title": "Карта нод",
+ "map_noNodesWithLocation": "Нет нод с данными о местоположении",
+ "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
+ "map_nodesCount": "Нод: {count}",
+ "map_pinsCount": "Меток: {count}",
+ "map_chat": "Чат",
+ "map_repeater": "Репитер",
+ "map_room": "Комната",
+ "map_sensor": "Сенсор",
+ "map_pinDm": "Метка (ЛС)",
+ "map_pinPrivate": "Метка (Приватная)",
+ "map_pinPublic": "Метка (Публичная)",
+ "map_lastSeen": "Последнее появление",
+ "map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
+ "map_from": "От",
+ "map_source": "Источник",
+ "map_flags": "Флаги",
+ "map_shareMarkerHere": "Поделиться меткой здесь",
+ "map_pinLabel": "Метка",
+ "map_label": "Подпись",
+ "map_pointOfInterest": "Точка интереса",
+ "map_sendToContact": "Отправить контакту",
+ "map_sendToChannel": "Отправить в канал",
+ "map_noChannelsAvailable": "Нет доступных каналов",
+ "map_publicLocationShare": "Публичная передача местоположения",
+ "map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.",
+ "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками",
+ "map_filterNodes": "Фильтр нод",
+ "map_nodeTypes": "Типы нод",
+ "map_chatNodes": "Чат-ноды",
+ "map_repeaters": "Репитеры",
+ "map_otherNodes": "Другие ноды",
+ "map_keyPrefix": "Префикс ключа",
+ "map_filterByKeyPrefix": "Фильтр по префиксу ключа",
+ "map_publicKeyPrefix": "Префикс публичного ключа",
+ "map_markers": "Метки",
+ "map_showSharedMarkers": "Показывать общие метки",
+ "map_lastSeenTime": "Время последнего появления",
+ "map_sharedPin": "Общая метка",
+ "map_joinRoom": "Присоединиться к комнате",
+ "map_manageRepeater": "Управление репитером",
+ "mapCache_title": "Кэш офлайн-карты",
+ "mapCache_selectAreaFirst": "Сначала выберите область для кэширования",
+ "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области",
+ "mapCache_downloadTilesTitle": "Загрузить плитки",
+ "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?",
+ "mapCache_downloadAction": "Загрузить",
+ "mapCache_cachedTiles": "Закэшировано {count} плиток",
+ "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)",
+ "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш",
+ "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?",
+ "mapCache_offlineCacheCleared": "Офлайн-кэш очищен",
+ "mapCache_noAreaSelected": "Область не выбрана",
+ "mapCache_cacheArea": "Область кэширования",
+ "mapCache_useCurrentView": "Использовать текущий вид",
+ "mapCache_zoomRange": "Диапазон масштаба",
+ "mapCache_estimatedTiles": "Оценочное количество плиток: {count}",
+ "mapCache_downloadedTiles": "Загружено {completed} из {total}",
+ "mapCache_downloadTilesButton": "Загрузить плитки",
+ "mapCache_clearCacheButton": "Очистить кэш",
+ "mapCache_failedDownloads": "Неудачных загрузок: {count}",
+ "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
+ "time_justNow": "Только что",
+ "time_minutesAgo": "{minutes} мин назад",
+ "time_hoursAgo": "{hours} ч назад",
+ "time_daysAgo": "{days} дн. назад",
+ "time_hour": "час",
+ "time_hours": "часов",
+ "time_day": "день",
+ "time_days": "дней",
+ "time_week": "неделя",
+ "time_weeks": "недель",
+ "time_month": "месяц",
+ "time_months": "месяцев",
+ "time_minutes": "минут",
+ "time_allTime": "Всё время",
+ "dialog_disconnect": "Отключиться",
+ "dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
+ "login_repeaterLogin": "Вход в репитер",
+ "login_roomLogin": "Вход на сервер комнат",
+ "login_password": "Пароль",
+ "login_enterPassword": "Введите пароль",
+ "login_savePassword": "Сохранить пароль",
+ "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве",
+ "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.",
+ "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.",
+ "login_routing": "Маршрутизация",
+ "login_routingMode": "Режим маршрутизации",
+ "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
+ "login_forceFloodMode": "Принудительный режим рассылки",
+ "login_managePaths": "Управление маршрутами",
+ "login_login": "Войти",
+ "login_attempt": "Попытка {current}/{max}",
+ "login_failed": "Ошибка входа: {error}",
+ "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.",
+ "common_reload": "Обновить",
+ "common_clear": "Очистить",
+ "path_currentPath": "Текущий маршрут: {path}",
+ "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
+ "path_enterCustomPath": "Введите маршрут вручную",
+ "path_currentPathLabel": "Текущий маршрут",
+ "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.",
+ "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)",
+ "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)",
+ "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)",
+ "path_selectFromContacts": "Или выберите из контактов:",
+ "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.",
+ "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.",
+ "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}",
+ "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.",
+ "path_setPath": "Установить маршрут",
+ "repeater_management": "Управление репитером",
+ "room_management": "Управление сервером комнат",
+ "repeater_managementTools": "Инструменты управления",
+ "repeater_status": "Статус",
+ "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера",
+ "repeater_telemetry": "Телеметрия",
+ "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
+ "repeater_cli": "CLI",
+ "repeater_cliSubtitle": "Отправка команд репитеру",
+ "repeater_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": "Новая группа",
+ "@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": "Неправильный формат ссылки",
+ "@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 d6ea7d83..4e66af0a 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,59 @@
"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}\"",
+ "@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 09ee0bcf..805621b6 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": {
@@ -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.",
@@ -1092,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.",
@@ -1130,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.",
@@ -1159,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.",
@@ -1232,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}",
@@ -1466,23 +1478,79 @@
"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": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"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}"
+ "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": "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 novo QR kodo za posodabljanje ključa za {name}",
+ "community_updateSecret": "Ažuriraj ključ",
+ "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 4d302a56..da017bed 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,59 @@
"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",
+ "@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
new file mode 100644
index 00000000..85ce4a26
--- /dev/null
+++ b/lib/l10n/app_uk.arb
@@ -0,0 +1,1557 @@
+{
+ "@@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}» оновлено",
+ "@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 c0704140..5f0c7977 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,59 @@
"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}”",
+ "@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/lib/models/contact.dart b/lib/models/contact.dart
index 364defff..c9e40ab7 100644
--- a/lib/models/contact.dart
+++ b/lib/models/contact.dart
@@ -102,6 +102,46 @@ 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];
+ }
+ }
+ }
+ return traceBytes;
+ }
+
Uint8List get _pathBytesForDisplay {
if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0);
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/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart
index 46648433..083a60b7 100644
--- a/lib/screens/channel_chat_screen.dart
+++ b/lib/screens/channel_chat_screen.dart
@@ -3,11 +3,15 @@ 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 '../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';
@@ -15,6 +19,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';
@@ -33,42 +38,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;
@@ -153,10 +167,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(
@@ -190,20 +200,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,
+ ),
+ ],
);
},
),
@@ -241,7 +282,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,
),
@@ -255,15 +298,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),
@@ -272,60 +320,84 @@ 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
- 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),
- 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],
- ),
- ],
- ],
+ ),
),
],
),
@@ -364,8 +436,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) {
@@ -690,14 +761,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),
@@ -711,9 +784,11 @@ class _ChannelChatScreenState extends State {
return TextField(
controller: _textController,
+ focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
+ textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder(
@@ -803,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),
@@ -852,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/channels_screen.dart b/lib/screens/channels_screen.dart
index 22fb7682..02567bb8 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,
+ );
+ },
+ ),
),
],
);
@@ -299,13 +312,15 @@ class _ChannelsScreenState extends State
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddChannelDialog(context),
+ tooltip: context.l10n.channels_addChannel,
child: const Icon(Icons.add),
),
bottomNavigationBar: SafeArea(
top: false,
child: QuickSwitchBar(
selectedIndex: 1,
- onDestinationSelected: (index) => _handleQuickSwitch(index, context),
+ onDestinationSelected: (index) =>
+ _handleQuickSwitch(index, context),
),
),
),
@@ -315,33 +330,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 +375,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 +405,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 +475,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 +499,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 +596,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 +625,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 +637,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 +665,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 +707,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 +729,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 +751,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 +774,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 +788,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 +810,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 +827,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 +844,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 +865,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,
+ ),
+ ),
);
}
},
@@ -828,27 +904,35 @@ class _ChannelsScreenState extends State
RadioGroup(
groupValue: isRegularHashtag,
onChanged: (v) => setDialogState(() {
- if (v != null) {
- isRegularHashtag = v;
- if (isRegularHashtag) {
- selectedCommunity = null;
- } else if (selectedCommunity == null && _communities.isNotEmpty) {
- selectedCommunity = _communities.first;
- }
+ if (v == null) return;
+ isRegularHashtag = v;
+ if (isRegularHashtag) {
+ selectedCommunity = null;
+ } else if (selectedCommunity == null &&
+ _communities.isNotEmpty) {
+ selectedCommunity = _communities.first;
}
}),
child: Column(
children: [
RadioListTile(
value: true,
- title: Text(dialogContext.l10n.community_regularHashtag),
- subtitle: Text(dialogContext.l10n.community_regularHashtagDesc),
+ 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),
+ title: Text(
+ dialogContext.l10n.community_communityHashtag,
+ ),
+ subtitle: Text(
+ dialogContext.l10n.community_communityHashtagDesc,
+ ),
dense: true,
),
],
@@ -858,22 +942,36 @@ class _ChannelsScreenState extends State
// 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 +997,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,12 +1008,20 @@ 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);
@@ -927,25 +1036,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;
}
channelName = '${selectedCommunity!.name} #$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,
+ ),
+ ),
+ ),
);
}
},
@@ -960,7 +1090,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(
@@ -968,15 +1101,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
}
}
},
@@ -992,7 +1126,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(
@@ -1011,10 +1148,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),
@@ -1025,47 +1168,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,
+ ),
);
}
},
@@ -1096,7 +1260,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),
@@ -1104,7 +1269,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) ...[
@@ -1113,7 +1279,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()!,
],
@@ -1122,7 +1289,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),
@@ -1170,7 +1338,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,
@@ -1228,7 +1398,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;
}
@@ -1237,7 +1409,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),
@@ -1257,7 +1431,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),
@@ -1268,10 +1444,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),
+ ),
),
],
),
@@ -1325,16 +1508,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,
),
],
@@ -1347,8 +1540,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(
@@ -1363,10 +1561,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);
}
@@ -1382,32 +1576,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,
@@ -1438,128 +1614,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) {
@@ -1568,16 +1639,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(
@@ -1595,19 +1668,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),
+ ),
+ ),
);
}
},
@@ -1621,32 +1698,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..cf343818 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -5,11 +5,15 @@ 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/reaction_helper.dart';
+import '../helpers/chat_scroll_controller.dart';
+import '../helpers/link_handler.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
@@ -20,6 +24,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';
@@ -36,25 +41,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();
@@ -167,9 +191,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),
],
@@ -201,13 +232,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(
@@ -256,13 +311,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),
@@ -276,9 +333,11 @@ class _ChatScreenState extends State {
return TextField(
controller: _textController,
+ focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
+ textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: const OutlineInputBorder(),
@@ -336,16 +395,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,
- );
- }
- });
}
@@ -802,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),
@@ -883,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);
}
}
@@ -957,7 +1012,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,
),
@@ -969,75 +1026,103 @@ 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
- 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),
- 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/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart
index e91cd943..d12abb07 100644
--- a/lib/screens/contacts_screen.dart
+++ b/lib/screens/contacts_screen.dart
@@ -1,6 +1,8 @@
import 'dart:async';
+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,7 +53,7 @@ class _ContactsScreenState extends State
final ContactGroupStore _groupStore = ContactGroupStore();
List _groups = [];
Timer? _searchDebounce;
-
+
@override
void initState() {
super.initState();
@@ -313,6 +315,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();
}
@@ -752,7 +762,19 @@ class _ContactsScreenState extends State
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- if (isRepeater)
+ if (isRepeater) ...[
+ ListTile(
+ leading: const Icon(Icons.radar, color: Colors.green),
+ title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
+ 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(
leading: const Icon(Icons.cell_tower, color: Colors.orange),
title: Text(context.l10n.contacts_manageRepeater),
@@ -761,7 +783,19 @@ class _ContactsScreenState extends State
_showRepeaterLogin(context, contact);
},
)
- else if (isRoom) ...[
+ ]else if (isRoom) ...[
+ ListTile(
+ leading: const Icon(Icons.radar, color: Colors.green),
+ title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
+ 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(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
@@ -778,7 +812,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),
@@ -798,6 +845,7 @@ class _ContactsScreenState extends State
_confirmDelete(context, connector, contact);
},
),
+ ],
],
),
),
@@ -852,8 +900,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),
@@ -861,23 +907,32 @@ 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,
- 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,
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),
),
),
diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart
index 5a545f37..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.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/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart
index 63f4a3c0..0d38d98b 100644
--- a/lib/screens/scanner_screen.dart
+++ b/lib/screens/scanner_screen.dart
@@ -8,9 +8,47 @@ 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 _changedNavigation = false;
+ late final VoidCallback _connectionListener;
+
+ @override
+ void initState() {
+ super.initState();
+ final connector = Provider.of(context, listen: false);
+
+ _connectionListener = () {
+ if (connector.state == MeshCoreConnectionState.disconnected) {
+ _changedNavigation = false;
+ } else if (connector.state == MeshCoreConnectionState.connected && !_changedNavigation) {
+ _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
Widget build(BuildContext context) {
return Scaffold(
@@ -161,15 +199,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(
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;
}
diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart
index 09039f07..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,10 +203,10 @@ class NotificationService {
macOS: macDetails,
);
- final preview = _truncateMessage(message, 30);
+ final preview = message.trim();
final body = preview.isEmpty
? 'Received new message'
- : 'Received new message: $preview';
+ : preview;
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
@@ -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) {
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/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),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart
new file mode 100644
index 00000000..958258bc
--- /dev/null
+++ b/lib/widgets/path_trace_dialog.dart
@@ -0,0 +1,226 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../connector/meshcore_connector.dart';
+import '../connector/meshcore_protocol.dart';
+import '../models/contact.dart';
+import '../widgets/snr_indicator.dart';
+import '../l10n/l10n.dart';
+class PathTraceDialog extends StatefulWidget {
+
+ const PathTraceDialog({
+ super.key,
+ required this.title,
+ required this.path,
+ });
+
+ final String title;
+ final Uint8List path;
+
+ @override
+ State createState() => _PathTraceDialogState();
+}
+
+class _PathTraceDialogState extends State {
+ StreamSubscription? _frameSubscription;
+ Timer? _timeoutTimer;
+
+ bool _isLoading = false;
+ bool _failed2Loaded = false;
+ bool _hasData = false;
+ Uint8List _pathData = Uint8List(0);
+ Uint8List _snrData = Uint8List(0) ;
+ Map _pathContacts = {};
+
+ @override
+ void initState() {
+ super.initState();
+ _setupFrameListener();
+ _doPathTrace();
+ }
+
+ @override
+ void dispose() {
+ _frameSubscription?.cancel();
+ _timeoutTimer?.cancel();
+ super.dispose();
+ }
+
+ Future _doPathTrace() async {
+ if(mounted) {
+ setState(() {
+ _isLoading = true;
+ _failed2Loaded = false;
+ });
+ }
+
+ final connector = Provider.of(context, listen: false);
+ final frame = buildTraceReq(
+ DateTime.now().millisecondsSinceEpoch ~/ 1000,
+ 0, //flags
+ 0, //auth
+ payload: widget.path,
+ );
+ connector.sendFrame(frame);
+ }
+
+ void _setupFrameListener() {
+ final connector = Provider.of(context, listen: false);
+ Uint8List tagData = Uint8List(4);
+ // Listen for incoming text messages from the repeater
+ _frameSubscription = connector.receivedFrames.listen((frame) {
+ if (frame.isEmpty) return;
+ final frameBuffer = BufferReader(frame);
+ final code = frameBuffer.readUInt8();
+
+ if (code == respCodeSent) {
+ frameBuffer.skipBytes(1); //reserved
+ tagData = frameBuffer.readBytes(4);
+ final timeoutSeconds = frameBuffer.readUInt32LE();
+
+ // Start timeout timer for trace response
+ _timeoutTimer?.cancel();
+ _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
+ if (!mounted) return;
+ setState(() {
+ _isLoading = false;
+ _failed2Loaded = true;
+ });
+ });
+ }
+
+ // Check if it's a binary response
+ if (code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) {
+ _timeoutTimer?.cancel();
+ if (!mounted) return;
+ frameBuffer.skipBytes(3); //reserved + path length + flag
+ if(listEquals(frameBuffer.readBytes(4), tagData)){
+ _handleTraceResponse(frame);
+ }
+ }
+ });
+ }
+
+ Future _handleTraceResponse(Uint8List frame)async {
+ final connector = Provider.of(context, listen: false);
+
+ final buffer = BufferReader(frame);
+ buffer.skipBytes(2); // Skip push code and reserved byte
+ int pathLength = buffer.readUInt8();
+ buffer.skipBytes(5); // Skip Flag byte and tag data
+ buffer.skipBytes(4); // Skip auth code
+ Uint8List pathData = buffer.readBytes(pathLength);
+ Uint8List snrData = buffer.readRemainingBytes();
+
+ Map pathContacts = {};
+
+ connector.contacts.where((c) => c.type != advTypeChat).forEach((
+ repeater,
+ ) {
+ for (var neighbourData in pathData) {
+ if (listEquals(
+ repeater.publicKey.sublist(0, 1),
+ Uint8List.fromList([neighbourData]),
+ )) {
+ pathContacts[neighbourData] = repeater;
+ }
+ }
+ });
+
+ setState(() {
+ _isLoading = false;
+ _hasData = true;
+ _pathData = pathData;
+ _snrData = snrData;
+ _pathContacts = pathContacts;
+ });
+ }
+
+ String formatDirectionText(int index) {
+ if (index == 0 || index == _snrData.length - 1) {
+ if (index == 0) {
+ return context.l10n.pathTrace_you;
+ } else {
+ return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}";
+ }
+ } else {
+ return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}";
+ }
+ }
+ String formatDirectionSubText(int index) {
+ if (index == 0 || index == _snrData.length - 1) {
+ if (index == 0) {
+ return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}";
+ } else {
+ return context.l10n.pathTrace_you;
+ }
+ } else {
+ return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}";
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ return AlertDialog(
+ title: Column( children: [
+ FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))),
+ if(_failed2Loaded)
+ Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),),
+ ],
+ ),
+ content: SafeArea(
+ child: RefreshIndicator(
+ onRefresh: _doPathTrace,
+ child: !_hasData
+ ? Center(
+ child: Text(l10n.pathTrace_notAvailable),
+ )
+ : ListView.builder(
+ itemCount: _snrData.length,
+ itemBuilder: (context, index) {
+ return Column(
+ children: [
+ ListTile(
+ leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made),
+ title: Text(
+ formatDirectionText(index), style: const TextStyle(fontSize: 14),
+ ),
+ subtitle: Text(
+ formatDirectionSubText(index),
+ style: const TextStyle(fontSize: 14),
+ ),
+ trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0),
+ onTap: () {
+ // Handle item tap
+ },
+ ),
+ if (index < _snrData.length - 1) const Divider(height: 0.0),
+ ],
+ );
+ },
+ ),
+ ),
+ ),
+ actions: [
+ IconButton(
+ icon: _isLoading
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.refresh),
+ onPressed: _isLoading ? null : _doPathTrace,
+ tooltip: l10n.pathTrace_refreshTooltip,
+ ),
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(l10n.common_close),
+ ),
+ ],
+ );
+ }
+}
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 838ecf8c..1d2554df 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,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(
@@ -382,6 +385,7 @@ class _RoomLoginDialogState extends State {
),
],
),
+ ),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
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/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/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index fdb93ad1..b4a41dd1 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -9,9 +9,9 @@ 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
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -19,8 +19,8 @@ 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"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
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 @@
+
+
diff --git a/pubspec.lock b/pubspec.lock
index de12f546..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:
@@ -262,6 +270,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:
@@ -317,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:
@@ -361,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:
@@ -397,6 +429,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:
@@ -421,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:
@@ -461,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:
@@ -469,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:
@@ -521,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:
@@ -613,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:
@@ -649,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:
@@ -818,6 +890,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 +1043,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.9.2 <4.0.0"
- flutter: ">=3.35.0"
+ dart: ">=3.10.3 <4.0.0"
+ flutter: ">=3.38.4"
diff --git a/pubspec.yaml b/pubspec.yaml
index 490c83d6..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.0+4
+version: 5.0.0+5
environment:
sdk: ^3.9.2
@@ -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/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));
+ });
+ });
+ });
+}
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
diff --git a/untranslated.json b/untranslated.json
index 2138a62f..b9dadf3e 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -1,121 +1,69 @@
{
"bg": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"de": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"es": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"fr": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"it": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"nl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"pl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"pt": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
+ ],
+
+ "ru": [
+ "appSettings_languageUk"
],
"sk": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"sl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
],
"sv": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
+ ],
+
+ "uk": [
+ "appSettings_languageRu"
],
"zh": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
+ "appSettings_languageRu",
+ "appSettings_languageUk"
]
}
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