diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9c0fc8fc..f1e6046b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; + DD31EC492B7F18B7006A3995 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = ""; }; DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -1071,6 +1072,7 @@ Base, "zh-Hans", pl, + he, ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( @@ -1372,6 +1374,7 @@ DDCDC6CE294821AD004C1DDA /* de */, A65FA974296876BF00A97686 /* zh-Hans */, DDF6B24B2A9C2FC800BA6931 /* pl */, + DD31EC492B7F18B7006A3995 /* he */, ); name = Localizable.strings; sourceTree = ""; @@ -1522,7 +1525,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.22; + MARKETING_VERSION = 2.2.23; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1556,7 +1559,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.22; + MARKETING_VERSION = 2.2.23; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1678,7 +1681,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.22; + MARKETING_VERSION = 2.2.23; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1711,7 +1714,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.22; + MARKETING_VERSION = 2.2.23; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 709517c2..ab973c1e 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -9,156 +9,51 @@ import Foundation import MapKit class OfflineTileManager: ObservableObject { - enum DownloadStatus { - case download, downloading, downloaded - } static let shared = OfflineTileManager() + init() { print("Documents Directory = \(documentsDirectory)") createDirectoriesIfNecessary() } + // MARK: - Private properties + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) } private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } private let fileManager = FileManager.default - // MARK: - Public property - var progress: Float = 0 - var status: DownloadStatus = .download + // MARK: - Public methods + func getAllDownloadedSize() -> String { fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles")) } - func hasBeenDownloaded(for boundingBox: MKMapRect) -> Bool { - getEstimatedDownloadSize(for: boundingBox) == 0 - } - func getEstimatedDownloadSize(for boundingBox: MKMapRect) -> Double { - let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) - let count = self.filterTilesAlreadyExisting(paths: paths).count - let size: Double = 30000 // Bytes (average size) - return Double(count) * size - } - func getDownloadedSize(for mapTileLink: MapTileServer) -> Double { - var accumulatedSize: UInt64 = 0 - let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) - let matchingTiles = mapTiles.filter { fileName in - let fileNameLower = fileName.absoluteString - return fileNameLower.contains(mapTileLink.id) - } - print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)") - for tile in matchingTiles { - let url = documentsDirectory.appendingPathComponent(tile.absoluteString) - accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 - } - return Double(accumulatedSize) - } - func getDownloadedSize(for boundingBox: MKMapRect) -> Double { - let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) - var accumulatedSize: UInt64 = 0 - for path in paths { - let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" - let url = documentsDirectory.appendingPathComponent(file) - accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 - } - return Double(accumulatedSize) - } + func removeAll() { try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles")) createDirectoriesIfNecessary() } - func remove(for mapTileLink: MapTileServer) { - let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) - let matchingTiles = mapTiles.filter { fileName in - let fileNameLower = fileName.absoluteString - return fileNameLower.contains(mapTileLink.id) + + func loadAndCacheTileOverlay(for path: MKTileOverlayPath) throws -> Data { + guard UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) else { + return try Data(contentsOf: Bundle.main.url(forResource: "alpha", withExtension: "png")!) } - print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)") - for tile in matchingTiles { - try? fileManager.removeItem(at: tile.absoluteURL) - } - } - func remove(for boundingBox: MKMapRect) { - let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) - for path in paths { - let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" - let url = documentsDirectory.appendingPathComponent(file) - try? fileManager.removeItem(at: url) - } - self.status = .download - } - /// Download and persist all tiles within the boundingBox - func download(boundingBox: MKMapRect, name: String) { - NetworkManager.shared.runIfNetwork { - self.status = .downloading - self.progress = 0.01 - let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) - let filteredPaths = self.filterTilesAlreadyExisting(paths: paths) - for i in 0.. URL { - let file = "\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" - // Check is tile is already available - let tilesUrl = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file) - if fileManager.fileExists(atPath: tilesUrl.path) { - return tilesUrl - } else { - if UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) { // Get and persist newTile - return persistLocally(path: path) - } else { // Else display empty tile (transparent over 铮縈aps tiles) - return Bundle.main.url(forResource: "alpha", withExtension: "png")! - } - } - } - // MARK: Private methods - private func computeTileOverlayPaths(boundingBox box: MKMapRect, maxZ: Int = 17) -> [MKTileOverlayPath] { - var paths = [MKTileOverlayPath]() - for z in 1...maxZ { - let topLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.minY).coordinate, zoom: z) - let topRight = tranformCoordinate(coordinates: MKMapPoint(x: box.maxX, y: box.minY).coordinate, zoom: z) - let bottomLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.maxY).coordinate, zoom: z) - for x in topLeft.x...topRight.x { - for y in topLeft.y...bottomLeft.y { - paths.append(MKTileOverlayPath(x: x, y: y, z: z, contentScaleFactor: 2)) - } - } - } - return paths - } - private func tranformCoordinate(coordinates: CLLocationCoordinate2D, zoom: Int) -> TileCoordinates { - let lng = coordinates.longitude - let lat = coordinates.latitude - let tileX = Int(floor((lng + 180) / 360.0 * pow(2.0, Double(zoom)))) - let tileY = Int(floor((1 - log( tan( lat * Double.pi / 180.0 ) + 1 / cos( lat * Double.pi / 180.0 )) / Double.pi ) / 2 * pow(2.0, Double(zoom)))) - return (tileX, tileY, zoom) - } - @discardableResult private func persistLocally(path: MKTileOverlayPath) -> URL { - let url = overlay.url(forTilePath: path) - let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" - let filename = documentsDirectory.appendingPathComponent(file) + + let tilesUrl = documentsDirectory + .appendingPathComponent("tiles") + .appendingPathComponent("\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y)") + .appendingPathExtension("png") + do { - let data = try Data(contentsOf: url) - try data.write(to: filename) - } catch { - print("馃拃 Save Tile Error = \(error)") - return url - } - return filename - } - private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] { - paths.filter { - let file = "\(UserDefaults.mapTileServer.id)-z\($0.z)x\($0.x)y\($0.y).png" - let tilesPath = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file).path - return !fileManager.fileExists(atPath: tilesPath) + return try Data(contentsOf: tilesUrl) + } catch let error as NSError where error.code == NSFileReadNoSuchFileError { + let data = try Data(contentsOf: overlay.url(forTilePath: path)) + try data.write(to: tilesUrl) + return data } } + + // MARK: Private methods + private func createDirectoriesIfNecessary() { let tiles = documentsDirectory.appendingPathComponent("tiles") try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:]) diff --git a/Meshtastic/Helpers/Map/TileOverlay.swift b/Meshtastic/Helpers/Map/TileOverlay.swift index 020e30a9..754771df 100644 --- a/Meshtastic/Helpers/Map/TileOverlay.swift +++ b/Meshtastic/Helpers/Map/TileOverlay.swift @@ -8,8 +8,8 @@ import Foundation import MapKit -typealias TileCoordinates = (x: Int, y: Int, z: Int) - class TileOverlay: MKTileOverlay { - override func url(forTilePath path: MKTileOverlayPath) -> URL { OfflineTileManager.shared.getTileOverlay(for: path) } + override func loadTile(at path: MKTileOverlayPath) async throws -> Data { + return try OfflineTileManager.shared.loadAndCacheTileOverlay(for: path) + } } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 080d9bfb..018be11c 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -203,7 +203,7 @@ enum HardwareModel: SwiftProtobuf.Enum { /// /// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT /// Newer V1.1, version is written on the PCB near the display. - case heltecWirelessTrackerV11 // = 48 + case heltecWirelessTracker // = 48 /// /// Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display @@ -307,7 +307,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 45: self = .betafpv2400Tx case 46: self = .betafpv900NanoTx case 47: self = .rpiPico - case 48: self = .heltecWirelessTrackerV11 + case 48: self = .heltecWirelessTracker case 49: self = .heltecWirelessPaper case 50: self = .tDeck case 51: self = .tWatchS3 @@ -367,7 +367,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .betafpv2400Tx: return 45 case .betafpv900NanoTx: return 46 case .rpiPico: return 47 - case .heltecWirelessTrackerV11: return 48 + case .heltecWirelessTracker: return 48 case .heltecWirelessPaper: return 49 case .tDeck: return 50 case .tWatchS3: return 51 @@ -432,7 +432,7 @@ extension HardwareModel: CaseIterable { .betafpv2400Tx, .betafpv900NanoTx, .rpiPico, - .heltecWirelessTrackerV11, + .heltecWirelessTracker, .heltecWirelessPaper, .tDeck, .tWatchS3, @@ -2622,7 +2622,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 45: .same(proto: "BETAFPV_2400_TX"), 46: .same(proto: "BETAFPV_900_NANO_TX"), 47: .same(proto: "RPI_PICO"), - 48: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_1"), + 48: .same(proto: "HELTEC_WIRELESS_TRACKER"), 49: .same(proto: "HELTEC_WIRELESS_PAPER"), 50: .same(proto: "T_DECK"), 51: .same(proto: "T_WATCH_S3"), diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 73f36095..1440c56a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -209,17 +209,19 @@ struct Connect: View { }.padding([.bottom, .top]) } } - .confirmationDialog("Connecting to a new radio will clear all local app data on the phone. The app may close.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + .confirmationDialog("Connecting to a new radio will clear all local app data on the phone and will reset all app settings.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { UserDefaults.preferredPeripheralId = selectedPeripherialId if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } - context.reset() + do { + clearCoreDataDatabase(context: context) PersistenceController.shared.clearDatabase() - + context.reset() + UserDefaults.standard.reset() } catch let error { print("馃挘 Failed to re-create CoreData database: " + error.localizedDescription) } diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index d388cb93..9173d93d 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -28,15 +28,16 @@ struct LoRaSignalStrengthMeter: View { .foregroundColor(getRssiColor(rssi: rssi)) .font(.caption2) } - .padding(.bottom, 2) } else { Gauge(value: Double(signalStrength.rawValue), in: 0...3) { } currentValueLabel: { Image(systemName: "dot.radiowaves.left.and.right") - .font(.caption) + .font(.callout) + .frame(width: 30) Text("Signal \(signalStrength.description)") - .font(.caption) + .font(.callout) .foregroundColor(.gray) + .fixedSize() } .gaugeStyle(.accessoryLinear) .tint(gradient) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8b0aa8ab..8c311d7c 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -225,7 +225,7 @@ struct ChannelMessageList: View { .padding(.bottom) .id(channel.allPrivateMessages.firstIndex(of: message)) - if currentUser && (message.ackError == 5 || message.ackError == 3) { + if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) { RetryButton(message: message) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index a8372f38..b595576f 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -203,7 +203,7 @@ struct UserMessageList: View { .padding(.bottom) .id(user.messageList.firstIndex(of: message)) - if currentUser && (message.receivedACK && !message.realACK) { + if currentUser && (message.ackError == 9 || message.ackError == 5 || message.ackError == 3) || (message.receivedACK && !message.realACK) { RetryButton(message: message) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 9365cffa..615d54ce 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -21,7 +21,9 @@ struct NodeListItem: View { LazyVStack(alignment: .leading) { HStack { VStack(alignment: .leading) { - CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) + .padding(.trailing, 5) + BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) .padding(.trailing, 5) } VStack(alignment: .leading) { @@ -41,7 +43,10 @@ struct NodeListItem: View { .font(.callout) .symbolRenderingMode(.hierarchical) .foregroundColor(.green) - Text("connected").font(.callout) + .frame(width: 30, height: 15) + Text("connected") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } HStack { @@ -49,8 +54,9 @@ struct NodeListItem: View { .font(.callout) .symbolRenderingMode(.hierarchical) .foregroundColor(node.isOnline ? .green : .orange) + .frame(width: 30, height: 20) LastHeardText(lastHeard: node.lastHeard) - .font(.caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) } HStack { @@ -58,8 +64,9 @@ struct NodeListItem: View { Image(systemName: role?.systemName ?? "figure") .font(.callout) .symbolRenderingMode(.hierarchical) + .frame(width: 30, height: 20) Text("Role: \(role?.name ?? "unknown".localized)") - .font(.caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) } if node.isStoreForwardRouter { @@ -67,8 +74,9 @@ struct NodeListItem: View { Image(systemName: "envelope.arrow.triangle.branch") .font(.callout) .symbolRenderingMode(.hierarchical) + .frame(width: 30, height: 20) Text("storeforward".localized) - .font(.caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) } } @@ -85,7 +93,9 @@ struct NodeListItem: View { Image(systemName: "lines.measurement.horizontal") .font(.callout) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.caption) + .frame(width: 30, height: 20) + DistanceText(meters: metersAway) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) } } @@ -98,7 +108,9 @@ struct NodeListItem: View { Image(systemName: "lines.measurement.horizontal") .font(.callout) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.caption) + .frame(width: 30, height: 20) + DistanceText(meters: metersAway) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) } } @@ -109,6 +121,7 @@ struct NodeListItem: View { Image(systemName: "fibrechannel") .font(.callout) .symbolRenderingMode(.hierarchical) + .frame(width: 30, height: 20) Text("Channel: \(node.channel)") .foregroundColor(.gray) .font(.caption) @@ -117,44 +130,62 @@ struct NodeListItem: View { Image(systemName: "network") .symbolRenderingMode(.hierarchical) .font(.callout) + .frame(width: 30, height: 20) Text("Via MQTT") .foregroundColor(.gray) .font(.caption) } } + if node.hasPositions || node.hasEnvironmentMetrics || node.hasDetectionSensorMetrics || node.hasTraceRoutes { + HStack { + Image(systemName: "scroll") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + Text("Logs:") + .foregroundColor(.gray) + .font(.callout) + if node.hasDeviceMetrics { + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + } + if node.hasPositions { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + } + if node.hasEnvironmentMetrics { + Image(systemName: "cloud.sun.rain") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + } + if node.hasDetectionSensorMetrics { + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + } + if node.hasTraceRoutes { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30, height: 20) + } + } + .padding(.top) + } if !connected { HStack { let preset = ModemPresets(rawValue: Int(modemPreset)) LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) - .padding(.top, 2) + } + .padding(.top) } - HStack { - BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) - - if node.hasPositions { - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .font(.callout) - - } - if node.hasEnvironmentMetrics { - Image(systemName: "cloud.sun.rain") - .symbolRenderingMode(.hierarchical) - .font(.callout) - } - if node.hasDetectionSensorMetrics { - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - .font(.callout) - } - if node.hasTraceRoutes { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - .font(.callout) - } - } - .padding(.top, 3) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index f1ce1aa3..bfaaa031 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -33,7 +33,7 @@ struct MQTTConfig: View { if node != nil && node?.loRaConfig != nil { let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0)) if rc?.dutyCycle ?? 0 <= 10 { - Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffice will quickly overwhelm your LoRa mesh.") + Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh.") .font(.callout) .foregroundColor(.red) } diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings new file mode 100644 index 00000000..f84e47a6 --- /dev/null +++ b/he.lproj/Localizable.strings @@ -0,0 +1,315 @@ +/* + Localizable.strings + Meshtastic + + Copyright(c) Garth Vander Houwen on 12/12/22. + +*/ +"about"="讗讜讚讜转"; +"about.meshtastic"="讗讜讚讜转 诪砖讟住讟讬拽"; +"admin"="讗讚诪讬谉"; +"admin.log"="讛讬住讟讜专讬讬转 讛讜讚注讜转 讗讚诪讬谉"; +"ago"="注讘专讜"; +"airtime"="讝诪谉 讗讜讜讬专"; +"always.on"="转诪讬讚 讚诇讜拽"; +"ambient.lighting"="转讗讜专转 住讘讬讘讛"; +"ambient.lighting.config"="讛讙讚专讜转 转讗讜专转 住讘讬讘讛"; +"appsettings"="讛讙讚专讜转 讗驻诇讬拽爪讬讛"; +"appsettings.provide.location"="砖转祝 诪讬拽讜诐"; +"appsettings.smartposition"="诪讬拽讜诐 讞讻诐"; +"are.you.sure"="讛讗诐 讗转讛 讘讟讜讞?"; +"ascii.capable"="讘注诇 讬讻讜诇转 ASCII"; +"available.radios"="诪讻砖讬专讬诐 讝诪讬谞讬诐"; +"automatic.detection"="讝讬讛讜讬 讗讜讟讜诪讟讬"; +"battery.level"="专诪转 住讜诇诇讛"; +"ble.name"="砖诐 讘诇讜讟讜住"; +"ble.connection.timeout %d %@"="讛转讞讘专讜转 谞讻砖诇讛 诇讗讞专 %d 谞住讬讜谞讜转 诇讛转讞讘专 诇%@. 讬转讻谉 讜讬砖 爪讜专讱 '诇砖讻讜讞' 讗转 讛诪讻砖讬专 讘讛讙讚专讜转 诪讻砖讬专 > 讘诇讜讟讜住."; +"ble.errorcode.6 %@"="%@ 讛讗驻诇讬拽爪讬讛 转谞住讛 讗讜讟讜诪讟讬转 诇讛转讞讘专 诪讞讚砖 诇诪讻砖讬专 讛诪讜注讚祝 讗诐 讬讬专讗讛."; +"ble.errorcode.14 %@"="%@ 砖讙讬讗讛 讝讜 讘讚专讱 讻诇诇 讗讬谞讛 谞讬转谞转 诇转讬拽讜谉 诇诇讗 砖讻讞讞转 讛诪讻砖讬专 讘讛讙讚专讜转 诪讻砖讬专 > 讘诇讜讟讜住 讜讗讝 诇讛转讞讘专 诪讞讚砖 诇诪讻砖讬专."; +"ble.errorcode.pin %@"="%@ 讘讘拽砖讛 谞住讛 砖谞讬转 诇讛转讞讘专 诇诪讻砖讬专 讜讘讚讜拽 讗转 讛拽讜讚."; +"bluetooth"="讘诇讜讟讜住"; +"bluetooth.off"="讘诇讜讟讜住 讻讘讜讬"; +"bluetooth.config"="讛讙讚专讜转 讘诇讜讟讜住"; +"bluetooth.mode.randompin"="拽讜讚 讗拽专讗讬"; +"bluetooth.mode.fixedpin"="拽讜讚 拽讘讜注"; +"bluetooth.mode.nopin"="诇诇讗 拽讜讚 (驻砖讜讟 注讜讘讚)"; +"bluetooth.pairingmode"="诪爪讘 讛爪诪讚讛"; +"bluetooth.pin.validation"="拽讜讚 讘诇讜讟讜住 讞讬讬讘转 诇讛讬讜转 讘转 6 住驻专讜转."; +"bytes"="讘讬讬讟讬诐"; +"cancel"="讘讟诇"; +"canned.messages"="讛讜讚注讜转 拽讘讜注讜转"; +"canned.messages.config"="讛讙讚专讜转 讛讜讚注讜转 拽讘讜注讜转"; +"canned.messages.preset.manual"="讛讙讚专讛 讬讚谞讬转"; +"canned.messages.preset.rakrotary"="RAK Rotary Encoder Module"; /* left untranslated for clarity */ +"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Keypad"; /* left untranslated for clarity */ +"channel"="注专讜抓"; +"channel.role.disabled"="讻讘讜讬"; +"channel.role.primary"="注讬拽专讬"; +"channel.role.secondary"="诪砖谞讬"; +"channel.utilization"="砖讬诪讜砖 注专讜抓"; +"channels"="注专讜爪讬诐"; +"clear.app.data"="讗驻住 讛讙讚专讜转 讗驻诇讬拽爪讬讛"; +"clear.log"="谞拽讛"; +"close"="住讙讜专"; +"config.save.confirm"="诇讗讞专 砖诪讬专转 讛讙讚专讜转 讛诪讻砖讬专 讬转讞讬诇 诪讞讚砖."; +"communicating"="诪转拽砖专 注诐 诪讻砖讬专. ."; +"connected.radio"="诪讻砖讬专 诪讞讜讘专"; +"connected"="诪讞讜讘专 讘讘诇讜讟讜住"; +"connecting"="诪转讞讘专 . ."; +"contacts"="讗谞砖讬 拽砖专"; +"contacts %@"="讗谞砖讬 拽砖专 (%@)"; +"copy"="讛注转拽"; +"current"="谞讜讻讞讬"; +"default"="讘专讬专转 诪讞讚诇"; +"delete"="诪讞拽"; +"detection.sensor"="讞讬讬砖谉 讝讬讛讜讬"; +"detection.sensor.config"="讛讙讚专讜转 讞讬讬砖谉 讝讬讛讜讬"; +"detection.sensor.log"="讬讜诪谉 讞讬讬砖谉 讝讬讛讜讬"; +"device"="诪讻砖讬专"; +"device.config"="讛讙讚专讜转 诪讻砖讬专"; +"device.metrics.delete"="谞拽讛 讬讜诪谉 诪讻砖讬专?"; +"device.metrics.log"="讬讜诪谉 诪讻砖讬专"; +"device.role.client"="讗驻诇讬拽爪讬讛 诪讞讜讘专转 讗讜 诪讻砖讬专 转拽砖讜专转 注爪诪讗讬."; +"device.role.clientmute"="诪讻砖讬专 砖讗讬谞讜 诪注讘讬专 讛讜讚注讜转 诪诪讻砖讬专讬诐 讗讞专讬诐 讛诇讗讛."; +"device.role.clienthidden"="诪讻砖讬专 砖专拽 诪砖讚专 诇驻讬 爪讜专讱 讘讻讚讬 诇讞住讜讱 讘讞砖诪诇 讗讜 诇砖诪讜专 注诇 讞砖讗讬讜转."; +"device.role.tracker"="诪砖讚专 诪讬拽讜诐 GPS 讘注讚讬驻讜转 讙讘讜讛讛."; +"device.role.lostandfound"="诪砖讚专 诪讬拽讜诐 讻讛讜讚注讛 诇注专讜抓 讘专讬专转 诪讞讚诇 诇注讬转讬诐 拽讘讜注讜转 讘讻讚讬 诇住讬讬注 讘诪爪讬讗转 讛诪讻砖讬专."; +"device.role.sensor"="诪砖讚专 讟诇诪讟专讬讛 讘注讚讬驻讜转 讙讘讜讛讛."; +"device.role.tak"="诪讜转讗诐 诇诪注专讻转 ATAK, 诪拽讟讬谉 转拽砖讜专转 拽讘讜注讛."; +"device.role.repeater"="诪讻砖讬专 转砖转讬转 诇讛专讞讘转 讛诪砖 注诇 讬讚讬 讛注讘专转 讛讜讚注讜转 注诐 讚讗讟讛 谞讜住祝 诪讬谞讬诪诇讬."; +"device.role.router"="诪讻砖讬专 转砖转讬转 诇讛专讞讘转 讛诪砖 注诇 讬讚讬 讛注讘专转 讛讜讚注讜转. 诪讜驻讬注 讘专砖讬诪转 诪讻砖讬专讬诐."; +"device.role.routerclient"="拽讜诪讘讬谞爪讬讛 砖诇 ROUTER 讜CLIENT. 诇讗 诇诪讻砖讬专讬诐 谞讬讬讚讬诐."; +"direct.messages"="讛讜讚注讛 驻专讟讬转"; +"dismiss.keyboard"="住讙讜专 诪拽诇讚转"; +"display"="爪讙 诪讻砖讬专"; +"display.config"="讛讙讚专讜转 爪讙"; +"distance"="诪专讞拽"; +"disconnect"="讛转谞转拽"; +"echo"="讛讚"; +"email.address"="讻转讜讘转 讚讜讗专 讗诇拽讟专讜谞讬"; +"enabled"="诪讜驻注诇"; +"encrypted"="诪讜爪驻谉"; +"external.notification"="谞讜讟讬驻讬拽爪讬讛 讞讬爪讜谞讬转"; +"external.notification.config"="讛讙讚专讜转 谞讜讟讬驻讬拽爪讬讛 讞讬爪讜谞讬转"; +"finish"="住讬讬诐"; +"firmware.version"="讙专住转 拽讜砖讞讛"; +"firmware.version.unsupported"="讙专住转 拽讜砖讞讛 讗讬谞讛 谞转诪讻转, 诇讗 谞讬转谉 诇讛转讞讘专 诇诪讻砖讬专."; +"gas"="讚诇拽"; +"gas.resistance"="Gas Resistance"; /* left untranslated for clarity */ +"generate.qr.code"="爪讜专 拽讜讚 QR"; +"gpsformat.dec"="驻讜专诪讟 拽讜讗讜专讚讬谞讟讜转"; +"gpsformat.dms"="诪注诇讜转 讚拽讜转 砖谞讬讜转"; +"gpsformat.utm"="Universal Transverse Mercator"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */ +"gpsformat.mgrs"="Military Grid Reference System"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */ +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */ +"gpsformat.osgr"="Ordnance Survey Grid Reference"; /* left untranslated for clarity. A translation exists but is frankly indecipherable */ +"gpsmode.disabled"="讻讘讜讬"; +"gpsmode.enabled"="诪讜驻注诇"; +"gpsmode.notPresent"="诇讗 拽讬讬诐"; +"heard"="谞砖诪注"; +"heard.last"="谞砖诪注 诇讗讞专讜谞讛"; +"hybrid"="讛讬讘专讬讚讬"; +"hybrid.flyover"="讛讬讘专讬讚讬 诪诇诪注诇讛"; +"include"="讻诇讜诇"; +"inputevent.none"="诇诇讗"; +"inputevent.up"="诇诪注诇讛"; +"inputevent.down"="诇诪讟讛"; +"inputevent.left"="砖诪讗诇讛"; +"inputevent.right"="讬诪讬谞讛"; +"inputevent.select"="讘讞专"; +"inputevent.back"="讗讞专讜讛"; +"inputevent.cancel"="讘讟诇"; +"interval.one.second"="砖谞讬讛 讗讞转"; +"interval.two.seconds"="砖转讬 砖谞讬讜转"; +"interval.three.seconds"="砖诇讜砖 砖谞讬讜转"; +"interval.four.seconds"="讗专讘注 砖谞讬讜转"; +"interval.five.seconds"="讞诪砖 砖谞讬讜转"; +"interval.ten.seconds"="注砖专 砖谞讬讜转"; +"interval.fifteen.seconds"="讞诪砖 注砖专讛 砖谞讬讜转"; +"interval.twenty.seconds"="注砖专讬诐 砖谞讬讜转"; +"interval.twentyfive.seconds"="注砖专讬诐 讜讞诪砖 砖谞讬讜转"; +"interval.thirty.seconds"="砖诇讜砖讬诐 砖谞讬讜转"; +"interval.fortyfive.seconds"="讗专讘注讬诐 讜讞诪砖 砖谞讬讜转"; +"interval.one.minute"="讚拽讛 讗讞转"; +"interval.two.minutes"="砖转讬 讚拽讜转"; +"interval.five.minutes"="讞诪砖 讚拽讜转"; +"interval.ten.minutes"="注砖专 讚拽讜转"; +"interval.fifteen.minutes"="讞诪砖 注砖专讛 讚拽讜转"; +"interval.thirty.minutes"="砖诇讜砖讬诐 讚拽讜转"; +"interval.one.hour"="砖注讛 讗讞转"; +"interval.two.hours"="砖注转讬讬诐"; +"interval.three.hours"="砖诇讜砖 砖注讜转"; +"interval.four.hours"="讗专讘注 砖注讜转"; +"interval.five.hours"="讞诪砖 砖注讜转"; +"interval.six.hours"="砖砖 砖注讜转"; +"interval.twelve.hours"="砖谞讬讬诐 注砖专 砖注讜转"; +"interval.eighteen.hours"="砖诪讜谞讛 注砖专 砖注讜转"; +"interval.twentyfour.hours"="注砖专讬诐 讜讗专讘注 砖注讜转"; +"interval.thirtysix.hours"="砖诇讜砖讬诐 讜砖砖 砖注讜转"; +"interval.fortyeight.hours"="讗专讘注讬诐 讜砖诪讜谞讛 砖注讜转"; +"interval.seventytwo.hours"="砖讘注讬诐 讜砖转讬讬诐 砖注讜转"; +"keyboard.type"="住讜讙 诪拽诇讚转"; +"logging"="专讬砖讜诐"; +"lora"="诇讜专讛"; +"lora.config"="讛讙讚专讜转 诇讜专讛"; +"map"="诪驻转 诪砖"; +"map.type"="住讜讙 讘专讬专转 诪讞讚诇"; +"map.centering"="诪讻砖讬专 讘诪专讻讝"; +"map.tiles.delete"="诪讞拽 讻诇 讞诇拽讬 诪驻讛 砖诪讜专讬诐"; +"map.recentering"="诪专讻讝 诪驻讛 讗讜讟讜诪讟讬转"; +"map.use.legacy"="讛砖转诪砖 讘诪驻讛 诪讚讜专 拽讜讚诐"; +"map.usertrackingmode"="诪爪讘 诪注拽讘 讗讞专 诪砖转诪砖"; +"map.usertrackingmode.follow"="注拽讜讘"; +"map.usertrackingmode.followwithheading"="注拽讜讘 注诐 讻讬讜讜谉"; +"map.usertrackingmode.none"="诇诇讗"; +"mesh.live.activity"="驻注讬诇讜转 诪砖 讞讬讛"; +"mesh.log"="讬讜诪谉 诪砖"; +"mesh.log.ambientlighting.config %@"="讛讙讚专讜转 诪讜讚讜诇转 转讗讜专转 住讘讬讘讛 讛转拽讘诇讜: %@"; +"mesh.log.bluetooth.config %@"="讛讙讚专讜转 讘诇讜讟讜住 讛转拽讘诇讜: %@"; +"mesh.log.cannedmessage.config %@"="讛讙讚专讜转 诪讜讚讜诇转 转讙讜讘讜转 砖诪讜专讜转 讛转拽讘诇讜: %@"; +"mesh.log.cannedmessages.messages.get %@"="讛转讘拽砖讜 讛讜讚注讜转 诪讜讚讜诇转 讛讜讚注讜转 砖诪讜专讜转 注讘讜专 诪讻砖讬专: %@"; +"mesh.log.cannedmessages.messages.received %@"="讛讜讚注讜转 注讘讜专 讛讜讚注讜转 砖诪讜专讜转 讛转拽讘诇讜 诪-%@"; +"mesh.log.channel.sent %@ %d"="谞砖诇讞 注专讜抓 注讘讜专: %@ 讗讬谞讚拽住 注专讜爪讬诐 %d"; +"mesh.log.channel.received %d %@"="注专讜抓 %d 讛转拽讘诇 诪-%@"; +"mesh.log.device.config %@"="讛讙讚专讜转 诪讻砖讬专 讛转拽讘诇讜: %@"; +"mesh.log.display.config %@"="讛讙讚专讜转 转爪讜讙讛 讛转拽讘诇讜: %@"; +"mesh.log.devicemetadata %@"="诪讘拽砖 诪讟讗-讚讗讟讛 注讘讜专 %@"; +"mesh.log.device.metadata.received %@"="诪讟讗-讚讗讟讛 砖诇 诪讻砖讬专 讛转拽讘诇 诪-%@"; +"mesh.log.detectionsensor.config %@"="讛讙讚专讜转 诪讜讚讜诇转 讞讬讬砖谉 讝讬讛讜讬 讛转拽讘诇讜: %@"; +"mesh.log.externalnotification.config %@"="讛讙讚专讜转 诪讜讚讜诇转 谞讜讟讬驻讬拽爪讬讛 讞讬爪讜谞讬转 讛转拽讘诇讜: %@"; +"mesh.log.lora.config %@"="讛讙讚专讜转 诇讜专讛 讛转拽讘诇讜: %@"; +"mesh.log.lora.config.sent %@"="谞砖诇讞讜 讛讙讚专讜转 诇讜专讛 注讘讜专: %@"; +"mesh.log.mqtt.config %@"="讛讙讚专讜转 诪讜讚讜诇转 MQTT 讛转拽讘诇讜: %@"; +"mesh.log.myinfo %@"="MyInfo 讛转拽讘诇: %@"; +"mesh.log.network.config %@"="讛讙讚专讜转 专砖转 讛转拽讘诇讜: %@"; +"mesh.log.nodeinfo.received %@"="诪讬讚注 讗讜讚讜转 诪讻砖讬专 讛转拽讘诇: %@"; +"mesh.log.position.config %@"="讛讙讚专讜转 诪讬拽讜诐 讛转拽讘诇讜: %@"; +"mesh.log.position.received %@"="讛讜讚注转 诪讬拽讜诐 讛转拽讘诇讜 诪-%@"; +"mesh.log.rangetest.config %@"="讛讙讚专讜转 诪讜讚讜诇转 讘讚讬拽转 讟讜讜讞 讛转拽讘诇讜: %@"; +"mesh.log.ringtone.config %@"="讛讙讚专讜转 RTTTL 专讬谞讙讟讜谉 讛转拽讘诇讜: %@"; +"mesh.log.routing.message %@ %@"="讛转拽讘诇 诪住诇讜诇 注讘讜专 讘拽砖讛: %@ 诪爪讘 砖诇讬讞讛: %@"; +"mesh.log.serial.config %@"="讛讙讚专讜转 诪讜讚讜诇转 转拽砖讜专转 住讬专讬讗诇讬转 讛转拽讘诇讜: %@"; +"mesh.log.sharelocation %@"="谞砖诇讞 诪讬拽讜诐 诪诪讻砖讬专 讛讗驻诇 诇诪讻砖讬专 讛诪砖讟住讟讬拽: %@"; +"mesh.log.storeforward.config %@"="讛讙讚专讜转 诪讜讚讜诇转 砖诪讬专讛 讜砖诇讬讞讛 讛转拽讘诇讜: %@"; +"mesh.log.telemetry.config %@"="讛讙讚专讜转 诪讜讚讜诇转 讟诇诪讟专讬讛 讛转拽讘诇讜: %@"; +"mesh.log.telemetry.received %@"="讛转拽讘诇 讟诇诪讟专讬讛 注讘讜专: %@"; +"mesh.log.textmessage.received"="讛讜讚注转 讟拽住讟 讛转拽讘诇讛."; +"mesh.log.textmessage.send.failed %@"="砖诇讬讞转 讛讜讚注讛 谞讻砖诇讛, 讗讬谉 讞讬讘讜专讬讜转 诇-%@"; +"mesh.log.textmessage.sent %@ %@ %@"="谞砖诇讞讛 讛讜讚注讛 %@ 诪-%@ 诇-%@"; +"mesh.log.traceroute.received.direct %@"="讘拽砖转 讘讚讬拽转 诪住诇讜诇 谞砖诇讞讛 诇诪讻砖讬专: %@ 讛转拽讘诇 讬砖讬专讜转."; +"mesh.log.traceroute.received.route %@"="讘拽砖转 讘讚讬拽转 诪住诇讜诇 讛爪诇讬讞讛: %@"; +"mesh.log.traceroute.sent %@"="谞砖诇讞讛 讘拽砖转 讘讚讬拽转 诪住诇讜诇 诇诪讻砖讬专: %@"; +"mesh.log.wantconfig %@"="砖讜诇讞 讘拽砖转 讛讙讚专讜转 诇-%@"; +"mesh.log.waypoint.sent %@"="谞砖诇讞讛 谞拽讜讚转 爪讬讜谉 诪-%@"; +"mesh.log.waypoint.received %@"="谞拽讜讚转 爪讬讜谉 讛转拽讘诇讛 诪-%@"; +"message"="讛讜讚注讛"; +"message.details"="驻专讟讬 讛讜讚注讛"; +"messages"="讛讜讚注讜转"; +"mode"="诪爪讘"; +"module.configuration"="讛讙讚专讜转 诪讜讚讜诇讛"; +"mqtt"="MQTT"; /*left untranslated for clarity */ +"mqtt.connect"="讛转讞讘专 诇-MQTT"; +"mqtt.config"="讛讙讚专讜转 MQTT"; +"mqtt.clientproxy"="MQTT Client Proxy"; /*left untranslated for clarity */ +"mqtt.disconnect"="讛转谞转拽 诪-MQTT"; +"mqtt.username"="砖诐 诪砖转诪砖"; +"name"="砖诐"; +"network"="专砖转"; +"network.config"="讛讙讚专讜转 专砖转"; +"nodes"="诪讻砖讬专讬诐"; +"nodes %@"="诪讻砖讬专讬诐 (%@)"; +"no.nodes"="诇讗 谞诪爪讗讜 诪讻砖讬专讬 诪砖讟住讟讬拽"; +"not.connected"="讗讬谉 诪讻砖讬专 诪讞讜讘专"; +"numbers.punctuation"="诪住驻专讬诐 讜住讬诪谞讬 驻讬住讜拽 "; +"off"="讻讘讜讬"; +"offline"="诪谞讜转拽"; +"on.boot"="专拽 讘注转 讛讚诇拽讛"; +"options"="讛讙讚专讜转"; +"password"="住讬住诪讗"; +"pause"="讛驻住拽"; +"phone.gps"="GPS 诪讛讟诇驻讜谉"; +"phone.gps.interval.description"="讻诇 讻诪讛 讝诪谉 诪讻砖讬专 讛讟诇驻讜谉 讬砖诇讞 讗转 诪讬拽讜诪讱 诇诪讻砖讬专 讛诪砖讟住讟讬拽. 注讚讻讜谞讬 诪讬拽讜诐 诇诪砖 诪谞讜讛诇讜转 注诇 讬讚讬 讛诪讻砖讬专."; +"position"="诪讬拽讜诐"; +"position.config"="讛讙讚专讜转 诪讬拽讜诐"; +"preferred.radio"="专讚讬讜 诪讜注讚祝"; +"radio.configuration"="讛讙讚专讜转 专讚讬讜"; +"range.test"="讘讚讬拽转 讟讜讜讞"; +"range.test.blocked"="讞住讜诐 讘讚讬拽讜转 讟讜讜讞"; +"range.test.config"="讛讙讚专讜转 讘讚讬拽转 讟讜讜讞"; +"reply"="转讙讜讘讛"; +"reboot"="讛转讞诇 诪讞讚砖"; +"reboot.node"="讛转讞诇 诪讻砖讬专 诪讞讚砖??"; +"received.ack"="讛转拽讘诇 讗讬砖讜专 诪住讬专讛"; +"received.ack.real"="讛转拽讘诇 讗讬砖讜专 诪住讬专讛 诪讛谞诪注谉"; +"resume"="讛讞诇 诪讞讚砖"; +"ringtone"="专讬谞讙讟讜谉"; +"ringtone.config"="讛讙讚专讜转 专讬谞讙讟讜谉"; +"route.recorder"="诪拽诇讬讟 诪住诇讜诇"; +"routes"="诪住诇讜诇讬诐"; +"routing.acknowledged"="诪讗砖专"; +"routing.noroute"="讗讬谉 诪住诇讜诇"; +"routing.gotnak"="讛转拽讘诇 讗讬砖讜专 诪住讬专讛 砖诇讬诇讬"; +"routing.timeout"="谞讙诪专 讛讝诪谉"; +"routing.nointerface"="讗讬谉 诪诪砖拽"; +"routing.maxretransmit"="讛讙讬注 诇诪拽住讬诪讜诐 讛砖诇讬讞讜转 诪讚砖"; +"routing.nochannel"="讗讬谉 注专讜抓"; +"routing.toolarge"="讛讛讜讚注讛 讗专讜讻讛/讙讚讜诇讛 诪讬讚讬"; +"routing.noresponse"="讗讬谉 转讙讜讘讛"; +"routing.dutycyclelimit"="讛讙讬注 诇诪拽住讬诪讜诐 砖讬诪讜砖 讗讝讜专讬 诇砖注讛 讝讜"; +"routing.badRequest"="讘拽砖讛 诇讗 转拽讬谞讛"; +"routing.notauthorized"="诇讗 诪讗讜砖专"; +"satellite"="诇讜讜讬谉"; +"satellite.flyover"="诇讜讜讬谉 讘砖诪讬讬诐"; +"save"="砖诪讜专"; +"save.config %@"="砖诪讜专 讛讙讚专讜转 注讘讜专 %@"; +"serial"="住讬专讬讗诇讬"; +"serial.config"="'讛讙讚专讜转 诪讜讚讜诇讛 '住讬专讬讗诇讬"; +"serial.mode.default"="讘专讬专转 诪讞讚诇"; +"serial.mode.simple"="驻砖讜讟"; +"serial.mode.proto"="Protobufs"; /*left untranslated for clarity */ +"serial.mode.txtmsg"="讛讜讚注转 讟拽住讟"; +"serial.mode.nmea"="诪讬拽讜诪讬 NMEA"; +"settings"="讛讙讚专讜转"; +"share.channels"="砖转祝 注专讜爪讬诐 讘讗诪爪注讜转 拽讜讚 QR"; +"share.position"="砖转祝 诪讬拽讜诐"; +"subscribed"="诪讞讜讘专 诇诪砖"; +"select.contact"="讘讞专 讗讬砖 拽砖专"; +"select.node"="讘讞专 诪讻砖讬专"; +"select.menu.item"="讘讞专 诪讛转驻专讬讟"; +"set.region"="讘讞专 讗讝讜专 诇讜专讛"; +"standard"="住讟谞讚专讟讬"; +"standard.muted"="住讟谞讚专转讬-讛砖转拽"; +"start"="讛讞诇"; +"storeforward"="砖诪讬专讛 讜砖诇讬讞讛"; +"storeforward.config"="讛讙讚专讜转 砖诪讬专讛 讜砖诇讬讞讛"; +"storeforward.heartbeat"="砖诇讞 讚讜驻拽"; +"ssid"="砖诐 专砖转 讜讜讬驻讬"; +"tapback"="转讙讜讘讛 诪讛讬专讛"; +"tapback.heart"="诇讘"; +"tapback.thumbsup"="讗讙讜讚诇 诇诪注诇讛"; +"tapback.thumbsdown"="讗讙讜讚诇 诇诪讟讛"; +"tapback.haha"="讞讞讞"; +"tapback.exclamation"="住讬诪谉 拽专讬讗讛"; +"tapback.question"="住讬诪谉 砖讗诇讛"; +"tapback.poop"="讞专讗"; +"telemetry"="讟诇诪讟专讬讛 (讞讬讬砖谞讬诐)"; +"telemetry.config"="讛讙讚专讜转 讟诇诪讟专讬讛"; +"timeout"="讝诪谉 拽爪讜讘"; +"timestamp"="砖注讛/转讗专讬讱"; +"tip.bluetooth.connect.title"="诪讻砖讬专 诪讞讜讘专"; +诪专讗讛 诪讬讚注 讗讜讚讜转 诪讻砖讬专 讛诪砖讟住讟讬拽 讛诪讞讜讘专 讻注转 诇讘诇讜讟讜住. 谞讬转谉 诇讙专讜专 砖诪讗诇讛 诇讛转谞转拽讜转 讗讜 诇讞讬爪讛 讗专讜讻讛 诇专讗讜转 住讟讟讬住讟讬拽讛 讗讜 诇讛转讞讬诇 驻注讬诇讜转. +"tip.bluetooth.connect.message"="诪专讗讛 诪讬讚注 讗讜讚讜转 诪讻砖讬专 讛诪砖讟住讟讬拽 讛诪讞讜讘专 讻注转 诇讘诇讜讟讜住. 谞讬转谉 诇讙专讜专 砖诪讗诇讛 诇讛转谞转拽讜转 讗讜 诇讞讬爪讛 讗专讜讻讛 诇专讗讜转 住讟讟讬住讟讬拽讛 讗讜 诇讛转讞讬诇 驻注讬诇讜转."; +"tip.channels.share.title"="诪砖转祝 注专讜爪讬 诪砖讟住讟讬拽"; +"tip.channels.share.message"="讘诪砖讟住讟讬拽 讬砖 注讚 8 注专讜爪讬诐. 讛专讗砖讜谉 讛讬谞讜 讛专讗砖讬 讜讛讬谞讜 讛讬讻谉 砖专讜讘 讛驻注讬诇讜转 诪转讘爪注转 讜讛讻专讞讬. 讗诐 诇讗 转砖转祝 讗转 讛注专讜抓 讛专讗砖讬 砖诇讱 讛注专讜抓 讛专讗砖讜谉 砖诇讱 谞讛讬讛 讛注专讜抓 讛专讗砖讬 讘专砖转 讛砖谞讬讛. 讛讜讗 诪讚讘专 讘注专讜抓 讛专讗砖讬 砖诇讜 讘诪砖谞讬 砖诇讱. 注专讜抓 讘注诇 讛砖诐 'admin' 讛讬谞讜 诇砖诇讬讟讛 诪专讞讜拽. 注专讜爪讬诐 谞讜住驻讬诐 讛讬谞诐 诇拽讘讜爪讜转 驻专讟讬讜转, 讻诇 讗讞转 注诐 诪驻转讞 讛爪驻谞讛 诪砖诇讛."; +"tip.messages.title"="讛讜讚注讜转"; +"tip.messages.message"="谞讬转谉 诇砖诇讜讞 讛讜讚注讜转 注专讜抓 (拽讘讜爪讜转 爪'讗讟) 讜讛讜讚注讜转 驻专讟讬讜转. 注诇 讛讜讚注讛 谞讬转谉 诇注砖讜转 诇讞讬爪讛 讗专讜讻讛 讘讻讚讬 诇专讗讜转 驻注讜诇讜转 讗驻砖专讬讜转 讻讙讜谉 讛注转拽, 讛讙讘, 转讙讜讘讛 诪讛讬专讛, 诪讞拽 讜讘谞讜住祝 诇专讗讜转 诪爪讘 砖诇讬讞讛."; +"twitter"="讟讜讜讬讟专"; +"unknown"="诇讗 讬讚讜注"; +"unknown.age"="讙讬诇 诇讗 讬讚讜注"; +"unset"="诇讗 谞拽讘注"; +"update.firmware"="注讚讻谉 拽讜砖讞讛"; +"update.interval"="讝诪谉 讘讬谉 注讚讻讜谞讬诐"; +"user"="诪砖转诪砖"; +"user.details"="驻专讟讬 诪砖转诪砖"; +"voltage"="讜讜诇讟讝'"; +"waiting"="诪诪转讬谉. . .";