From 256a1593cc36493d32500573b9126c16f3eba266 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:55:59 -0700 Subject: [PATCH 1/7] Migrate test project to Swift Testing and add connect view and router tests (#1643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen --- Meshtastic.xcodeproj/project.pbxproj | 4 + MeshtasticTests/ConnectViewTests.swift | 493 +++++++++++++++++++++++++ MeshtasticTests/RouterTests.swift | 314 ++++++++++++---- 3 files changed, 730 insertions(+), 81 deletions(-) create mode 100644 MeshtasticTests/ConnectViewTests.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4c264fbb..e3504190 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; }; 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; }; 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; }; + AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; 2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; }; 300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; }; 3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; }; @@ -411,6 +412,7 @@ 25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; + AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = ""; }; 3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = ""; }; 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = ""; }; @@ -900,6 +902,7 @@ 25F5D5C82C4375A8008036E3 /* MeshtasticTests */ = { isa = PBXGroup; children = ( + AA00010022E2730EC0060000 /* ConnectViewTests.swift */, 25F5D5D02C4375DF008036E3 /* RouterTests.swift */, ); path = MeshtasticTests; @@ -1657,6 +1660,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */, 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MeshtasticTests/ConnectViewTests.swift b/MeshtasticTests/ConnectViewTests.swift new file mode 100644 index 00000000..cbbcd331 --- /dev/null +++ b/MeshtasticTests/ConnectViewTests.swift @@ -0,0 +1,493 @@ +import Foundation +import SwiftUI +import Testing + +@testable import Meshtastic + +// MARK: - Device Tests + +@Suite("Device") +struct DeviceTests { + + static let testUUID = UUID(uuidString: "12345678-1234-1234-1234-123456789ABC")! + + @Test func creation() { + let device = Device( + id: DeviceTests.testUUID, + name: "Test Radio", + transportType: .ble, + identifier: "BLE-001" + ) + #expect(device.id == DeviceTests.testUUID) + #expect(device.name == "Test Radio") + #expect(device.transportType == .ble) + #expect(device.identifier == "BLE-001") + #expect(device.connectionState == .disconnected) + #expect(device.rssi == nil) + #expect(device.num == nil) + #expect(device.wasRestored == false) + #expect(device.isManualConnection == false) + } + + @Test func creationWithAllProperties() { + let device = Device( + id: DeviceTests.testUUID, + name: "Full Radio", + transportType: .tcp, + identifier: "192.168.1.1:4403", + connectionState: .connected, + rssi: -60, + num: 123456, + wasRestored: true, + isManualConnection: true + ) + #expect(device.connectionState == .connected) + #expect(device.rssi == -60) + #expect(device.num == 123456) + #expect(device.wasRestored == true) + #expect(device.isManualConnection == true) + } + + @Test(arguments: [ + (-50, BLESignalStrength.strong), + (-64, BLESignalStrength.strong), + (-65, BLESignalStrength.normal), + (-80, BLESignalStrength.normal), + (-84, BLESignalStrength.normal), + (-85, BLESignalStrength.weak), + (-100, BLESignalStrength.weak), + ]) + func signalStrength(rssi: Int, expected: BLESignalStrength) { + let device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001", + rssi: rssi + ) + #expect(device.getSignalStrength() == expected) + } + + @Test func signalStrengthNilWhenNoRSSI() { + let device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + #expect(device.getSignalStrength() == nil) + } + + @Test func rssiStringWithValue() { + var device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001", + rssi: -72 + ) + #expect(device.rssiString == "-72 dBm") + + device.rssi = -100 + #expect(device.rssiString == "-100 dBm") + } + + @Test func rssiStringWithoutValue() { + let device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + #expect(device.rssiString == "n/a") + } + + @Test func descriptionWithBothNames() { + var device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + device.shortName = "TST" + device.longName = "Test Node" + #expect(device.description == "Test Node (TST)") + } + + @Test func descriptionWithShortNameOnly() { + var device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + device.shortName = "TST" + #expect(device.description == "TST") + } + + @Test func descriptionWithLongNameOnly() { + var device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + device.longName = "Test Node" + #expect(device.description == "Test Node") + } + + @Test func descriptionWithNoNames() { + let device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + #expect(device.description == "Device(id: \(DeviceTests.testUUID))") + } + + @Test func hashEquality() { + let device1 = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + let device2 = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001" + ) + #expect(device1 == device2) + #expect(device1.hashValue == device2.hashValue) + } + + @Test func codableRoundTrip() throws { + var device = Device( + id: DeviceTests.testUUID, + name: "Radio", + transportType: .ble, + identifier: "BLE-001", + connectionState: .connected, + rssi: -70, + num: 99 + ) + device.shortName = "RDO" + device.longName = "My Radio" + device.firmwareVersion = "2.5.0" + + let data = try JSONEncoder().encode(device) + let decoded = try JSONDecoder().decode(Device.self, from: data) + + #expect(decoded.id == device.id) + #expect(decoded.name == device.name) + #expect(decoded.transportType == device.transportType) + #expect(decoded.identifier == device.identifier) + #expect(decoded.connectionState == device.connectionState) + #expect(decoded.rssi == device.rssi) + #expect(decoded.num == device.num) + #expect(decoded.shortName == device.shortName) + #expect(decoded.longName == device.longName) + #expect(decoded.firmwareVersion == device.firmwareVersion) + } +} + +// MARK: - TransportType Tests + +@Suite("TransportType") +struct TransportTypeTests { + + @Test func allCases() { + let cases = TransportType.allCases + #expect(cases.count == 3) + #expect(cases.contains(.ble)) + #expect(cases.contains(.tcp)) + #expect(cases.contains(.serial)) + } + + @Test(arguments: [ + (TransportType.ble, "BLE"), + (TransportType.tcp, "TCP"), + (TransportType.serial, "Serial"), + ]) + func rawValues(type: TransportType, expected: String) { + #expect(type.rawValue == expected) + } + + @Test func initFromRawValue() { + #expect(TransportType(rawValue: "BLE") == .ble) + #expect(TransportType(rawValue: "TCP") == .tcp) + #expect(TransportType(rawValue: "Serial") == .serial) + #expect(TransportType(rawValue: "invalid") == nil) + } + + @Test func codableRoundTrip() throws { + for type in TransportType.allCases { + let data = try JSONEncoder().encode(type) + let decoded = try JSONDecoder().decode(TransportType.self, from: data) + #expect(decoded == type) + } + } +} + +// MARK: - ConnectionState Tests + +@Suite("ConnectionState") +struct ConnectionStateTests { + + @Test func equality() { + #expect(ConnectionState.disconnected == .disconnected) + #expect(ConnectionState.connecting == .connecting) + #expect(ConnectionState.connected == .connected) + #expect(ConnectionState.disconnected != .connected) + #expect(ConnectionState.connecting != .disconnected) + } + + @Test func codableRoundTrip() throws { + let states: [ConnectionState] = [.disconnected, .connecting, .connected] + for state in states { + let data = try JSONEncoder().encode(state) + let decoded = try JSONDecoder().decode(ConnectionState.self, from: data) + #expect(decoded == state) + } + } +} + +// MARK: - BLESignalStrength Tests + +@Suite("BLESignalStrength") +struct BLESignalStrengthTests { + + @Test func rawValues() { + #expect(BLESignalStrength.weak.rawValue == 0) + #expect(BLESignalStrength.normal.rawValue == 1) + #expect(BLESignalStrength.strong.rawValue == 2) + } + + @Test func initFromRawValue() { + #expect(BLESignalStrength(rawValue: 0) == .weak) + #expect(BLESignalStrength(rawValue: 1) == .normal) + #expect(BLESignalStrength(rawValue: 2) == .strong) + #expect(BLESignalStrength(rawValue: 3) == nil) + } +} + +// MARK: - TransportStatus Tests + +@Suite("TransportStatus") +struct TransportStatusTests { + + @Test func equality() { + #expect(TransportStatus.uninitialized == .uninitialized) + #expect(TransportStatus.ready == .ready) + #expect(TransportStatus.discovering == .discovering) + #expect(TransportStatus.error("test") == .error("test")) + #expect(TransportStatus.error("a") != .error("b")) + #expect(TransportStatus.ready != .discovering) + } +} + +// MARK: - NavigationState Tests + +@Suite("NavigationState") +struct NavigationStateTests { + + @Test func defaultState() { + let state = NavigationState() + #expect(state.selectedTab == .connect) + #expect(state.messages == nil) + #expect(state.nodeListSelectedNodeNum == nil) + #expect(state.map == nil) + #expect(state.settings == nil) + } + + @Test(arguments: [ + NavigationState.Tab.messages, + NavigationState.Tab.connect, + NavigationState.Tab.nodes, + NavigationState.Tab.map, + NavigationState.Tab.settings, + ]) + func tabRawValues(tab: NavigationState.Tab) { + #expect(NavigationState.Tab(rawValue: tab.rawValue) == tab) + } + + @Test func messagesNavigationState() { + let channels = MessagesNavigationState.channels(channelId: 1, messageId: 100) + let directMessages = MessagesNavigationState.directMessages(userNum: 42, messageId: 200) + + let state1 = NavigationState(selectedTab: .messages, messages: channels) + let state2 = NavigationState(selectedTab: .messages, messages: directMessages) + + #expect(state1 != state2) + #expect(state1.messages != nil) + #expect(state2.messages != nil) + } + + @Test func mapNavigationState() { + let selectedNode = MapNavigationState.selectedNode(12345) + let waypoint = MapNavigationState.waypoint(67890) + + #expect(selectedNode != waypoint) + #expect(MapNavigationState.selectedNode(12345) == selectedNode) + } + + @Test func settingsNavigationState() { + #expect(SettingsNavigationState(rawValue: "about") == .about) + #expect(SettingsNavigationState(rawValue: "appSettings") == .appSettings) + #expect(SettingsNavigationState(rawValue: "lora") == .lora) + #expect(SettingsNavigationState(rawValue: "mqtt") == .mqtt) + #expect(SettingsNavigationState(rawValue: "nonexistent") == nil) + } + + @Test func hashable() { + let state1 = NavigationState(selectedTab: .connect) + let state2 = NavigationState(selectedTab: .connect) + let state3 = NavigationState(selectedTab: .messages) + + #expect(state1 == state2) + #expect(state1 != state3) + #expect(state1.hashValue == state2.hashValue) + } +} + +// MARK: - InvalidVersion View Tests + +@Suite("InvalidVersion") +struct InvalidVersionTests { + + @Test func viewCreation() { + let view = InvalidVersion(minimumVersion: "2.5.0", version: "2.3.0") + #expect(view.minimumVersion == "2.5.0") + #expect(view.version == "2.3.0") + } + + @Test func viewCreationWithEmptyVersions() { + let view = InvalidVersion() + #expect(view.minimumVersion == "") + #expect(view.version == "") + } +} + +// MARK: - ConnectedDevice View Tests + +@Suite("ConnectedDevice") +struct ConnectedDeviceTests { + + @Test func connectedState() { + let view = ConnectedDevice(deviceConnected: true, name: "TEST") + #expect(view.deviceConnected == true) + #expect(view.name == "TEST") + #expect(view.mqttProxyConnected == false) + #expect(view.showActivityLights == true) + } + + @Test func disconnectedState() { + let view = ConnectedDevice(deviceConnected: false, name: "?") + #expect(view.deviceConnected == false) + #expect(view.name == "?") + } + + @Test func withMQTTOptions() { + let view = ConnectedDevice( + deviceConnected: true, + name: "MQTT", + mqttProxyConnected: true, + mqttUplinkEnabled: true, + mqttDownlinkEnabled: true, + mqttTopic: "msh/US/2/e/#" + ) + #expect(view.mqttProxyConnected == true) + #expect(view.mqttUplinkEnabled == true) + #expect(view.mqttDownlinkEnabled == true) + #expect(view.mqttTopic == "msh/US/2/e/#") + } + + @Test func phoneOnlyMode() { + let view = ConnectedDevice( + deviceConnected: true, + name: "PHON", + phoneOnly: true, + showActivityLights: false + ) + #expect(view.phoneOnly == true) + #expect(view.showActivityLights == false) + } +} + +// MARK: - CircleText View Tests + +@Suite("CircleText") +struct CircleTextTests { + + @Test func defaultCircleSize() { + let view = CircleText(text: "AB", color: .blue) + #expect(view.text == "AB") + #expect(view.circleSize == 45) + } + + @Test func customCircleSize() { + let view = CircleText(text: "XY", color: .red, circleSize: 90) + #expect(view.text == "XY") + #expect(view.circleSize == 90) + } + + @Test func emojiText() { + let view = CircleText(text: "😝", color: .orange, circleSize: 80) + #expect(view.text == "😝") + #expect(view.circleSize == 80) + } +} + +// MARK: - BatteryCompact View Tests + +@Suite("BatteryCompact") +struct BatteryCompactTests { + + @Test func creationWithLevel() { + let view = BatteryCompact(batteryLevel: 75, font: .caption, iconFont: .callout, color: .accentColor) + #expect(view.batteryLevel == 75) + } + + @Test func creationWithNilLevel() { + let view = BatteryCompact(batteryLevel: nil, font: .caption, iconFont: .callout, color: .accentColor) + #expect(view.batteryLevel == nil) + } + + @Test func pluggedInLevel() { + let view = BatteryCompact(batteryLevel: 101, font: .caption, iconFont: .callout, color: .accentColor) + #expect(view.batteryLevel! > 100) + } + + @Test func chargingLevel() { + let view = BatteryCompact(batteryLevel: 100, font: .caption, iconFont: .callout, color: .accentColor) + #expect(view.batteryLevel == 100) + } +} + +// MARK: - SignalStrengthIndicator View Tests + +@Suite("SignalStrengthIndicator") +struct SignalStrengthIndicatorTests { + + @Test func defaultDimensions() { + let view = SignalStrengthIndicator(signalStrength: .strong) + #expect(view.signalStrength == .strong) + #expect(view.width == 8) + #expect(view.height == 40) + } + + @Test func customDimensions() { + let view = SignalStrengthIndicator(signalStrength: .weak, width: 5, height: 20) + #expect(view.signalStrength == .weak) + #expect(view.width == 5) + #expect(view.height == 20) + } + + @Test(arguments: [BLESignalStrength.weak, .normal, .strong]) + func allStrengthLevels(strength: BLESignalStrength) { + let view = SignalStrengthIndicator(signalStrength: strength) + #expect(view.signalStrength == strength) + } +} diff --git a/MeshtasticTests/RouterTests.swift b/MeshtasticTests/RouterTests.swift index 96bd70af..1175dc59 100644 --- a/MeshtasticTests/RouterTests.swift +++ b/MeshtasticTests/RouterTests.swift @@ -1,148 +1,300 @@ import Foundation -import XCTest +import Testing @testable import Meshtastic -final class RouterTests: XCTestCase { +@Suite("Router") +struct RouterTests { - func testInitialState() async throws { + // MARK: - Initialization + + @Test func defaultInitialState() async { let router = await Router() + let state = await router.navigationState + #expect(state.selectedTab == .connect) + #expect(state.messages == nil) + #expect(state.nodeListSelectedNodeNum == nil) + #expect(state.map == nil) + #expect(state.settings == nil) + } + + @Test func customInitialState() async { + let custom = NavigationState(selectedTab: .map, map: .waypoint(42)) + let router = await Router(navigationState: custom) + let state = await router.navigationState + #expect(state == custom) + } + + // MARK: - Invalid URL Handling + + @Test func invalidSchemeIsIgnored() async throws { + let router = await Router() + let url = try #require(URL(string: "https:///messages")) + await router.route(url: url) let tab = await router.navigationState.selectedTab - XCTAssertEqual(tab, .connect) + #expect(tab == .connect) } - func testRouteMessages() async throws { - try await assertRoute( - router: Router(), - "meshtastic:///messages", - NavigationState(selectedTab: .messages) - ) + @Test func unknownPathIsIgnored() async throws { + let router = await Router() + let url = try #require(URL(string: "meshtastic:///unknown")) + await router.route(url: url) + let state = await router.navigationState + #expect(state == NavigationState(selectedTab: .connect)) } - func testRouteMessagesWithChannelIdAndMessageId() async throws { - try await assertRoute( - router: Router(), - "meshtastic:///messages?channelId=0&messageId=1122334455", - NavigationState( - selectedTab: .messages, - messages: .channels( - channelId: 0, - messageId: 1122334455 - ) - ) - ) - } + // MARK: - Connect - func testRouteMessagesWithUserNumAndMessageId() async throws { + @Test func routeConnect() async throws { try await assertRoute( - router: Router(), - "meshtastic:///messages?userNum=123456789&messageId=9876543210", - NavigationState( - selectedTab: .messages, - messages: .directMessages( - userNum: 123456789, - messageId: 9876543210 - ) - ) - ) - } - - func testRouteConnect() async throws { - try await assertRoute( - router: Router(), "meshtastic:///connect", NavigationState(selectedTab: .connect) ) } - func testRouteNodes() async throws { + // MARK: - Messages + + @Test func routeMessages() async throws { + try await assertRoute( + "meshtastic:///messages", + NavigationState(selectedTab: .messages) + ) + } + + @Test func routeMessagesWithChannelIdAndMessageId() async throws { + try await assertRoute( + "meshtastic:///messages?channelId=0&messageId=1122334455", + NavigationState( + selectedTab: .messages, + messages: .channels(channelId: 0, messageId: 1122334455) + ) + ) + } + + @Test func routeMessagesWithChannelIdOnly() async throws { + try await assertRoute( + "meshtastic:///messages?channelId=5", + NavigationState( + selectedTab: .messages, + messages: .channels(channelId: 5, messageId: nil) + ) + ) + } + + @Test func routeMessagesWithUserNumAndMessageId() async throws { + try await assertRoute( + "meshtastic:///messages?userNum=123456789&messageId=9876543210", + NavigationState( + selectedTab: .messages, + messages: .directMessages(userNum: 123456789, messageId: 9876543210) + ) + ) + } + + @Test func routeMessagesWithUserNumOnly() async throws { + try await assertRoute( + "meshtastic:///messages?userNum=42", + NavigationState( + selectedTab: .messages, + messages: .directMessages(userNum: 42, messageId: nil) + ) + ) + } + + @Test func routeMessagesWithOnlyMessageIdIgnoresIt() async throws { + try await assertRoute( + "meshtastic:///messages?messageId=999", + NavigationState(selectedTab: .messages) + ) + } + + @Test func routeMessagesWithNonNumericParamsIgnoresThem() async throws { + try await assertRoute( + "meshtastic:///messages?channelId=abc&messageId=xyz", + NavigationState(selectedTab: .messages) + ) + } + + // MARK: - Nodes + + @Test func routeNodes() async throws { try await assertRoute( - router: Router(), "meshtastic:///nodes", NavigationState(selectedTab: .nodes) ) } - func testRouteNodesWithNodeNum() async throws { + @Test func routeNodesWithNodeNum() async throws { try await assertRoute( - router: Router(), "meshtastic:///nodes?nodenum=1234567890", - NavigationState( - selectedTab: .nodes, - nodeListSelectedNodeNum: 1234567890 - ) + NavigationState(selectedTab: .nodes, nodeListSelectedNodeNum: 1234567890) ) } - func testRouteMap() async throws { + @Test func routeNodesWithNonNumericNodeNum() async throws { + try await assertRoute( + "meshtastic:///nodes?nodenum=abc", + NavigationState(selectedTab: .nodes) + ) + } + + // MARK: - Map + + @Test func routeMap() async throws { try await assertRoute( - router: Router(), "meshtastic:///map", NavigationState(selectedTab: .map) ) } - func testRouteMapWithWaypointId() async throws { + @Test func routeMapWithWaypointId() async throws { try await assertRoute( - router: Router(), "meshtastic:///map?waypointId=123456", - NavigationState( - selectedTab: .map, - map: .waypoint(123456) - ) + NavigationState(selectedTab: .map, map: .waypoint(123456)) ) } - func testRouteMapWithNodeNum() async throws { + @Test func routeMapWithNodeNum() async throws { try await assertRoute( - router: Router(), "meshtastic:///map?nodenum=1234567890", - NavigationState( - selectedTab: .map, - map: .selectedNode(1234567890) - ) + NavigationState(selectedTab: .map, map: .selectedNode(1234567890)) ) } - func testRouteSettings() async throws { + @Test func routeMapWithBothNodeNumAndWaypointIdPrefersNode() async throws { + try await assertRoute( + "meshtastic:///map?nodenum=111&waypointId=222", + NavigationState(selectedTab: .map, map: .selectedNode(111)) + ) + } + + @Test func routeMapWithNonNumericParamsIgnoresThem() async throws { + try await assertRoute( + "meshtastic:///map?nodenum=abc&waypointId=xyz", + NavigationState(selectedTab: .map) + ) + } + + // MARK: - Settings + + @Test func routeSettings() async throws { try await assertRoute( - router: Router(), "meshtastic:///settings", - NavigationState( - selectedTab: .settings - ) + NavigationState(selectedTab: .settings) ) } - func testRouteSettingsAbout() async throws { + @Test(arguments: [ + ("about", SettingsNavigationState.about), + ("appSettings", SettingsNavigationState.appSettings), + ("routes", SettingsNavigationState.routes), + ("routeRecorder", SettingsNavigationState.routeRecorder), + ("lora", SettingsNavigationState.lora), + ("channels", SettingsNavigationState.channels), + ("shareQRCode", SettingsNavigationState.shareQRCode), + ("user", SettingsNavigationState.user), + ("bluetooth", SettingsNavigationState.bluetooth), + ("device", SettingsNavigationState.device), + ("display", SettingsNavigationState.display), + ("network", SettingsNavigationState.network), + ("position", SettingsNavigationState.position), + ("power", SettingsNavigationState.power), + ("ambientLighting", SettingsNavigationState.ambientLighting), + ("cannedMessages", SettingsNavigationState.cannedMessages), + ("detectionSensor", SettingsNavigationState.detectionSensor), + ("externalNotification", SettingsNavigationState.externalNotification), + ("mqtt", SettingsNavigationState.mqtt), + ("rangeTest", SettingsNavigationState.rangeTest), + ("paxCounter", SettingsNavigationState.paxCounter), + ("ringtone", SettingsNavigationState.ringtone), + ("serial", SettingsNavigationState.serial), + ("security", SettingsNavigationState.security), + ("storeAndForward", SettingsNavigationState.storeAndForward), + ("telemetry", SettingsNavigationState.telemetry), + ("debugLogs", SettingsNavigationState.debugLogs), + ("appFiles", SettingsNavigationState.appFiles), + ("firmwareUpdates", SettingsNavigationState.firmwareUpdates), + ("tak", SettingsNavigationState.tak), + ]) + func routeSettingsPage(path: String, expected: SettingsNavigationState) async throws { try await assertRoute( - router: Router(), - "meshtastic:///settings/about", - NavigationState( - selectedTab: .settings, - settings: .about - ) + "meshtastic:///settings/\(path)", + NavigationState(selectedTab: .settings, settings: expected) ) } - func testRouteSettingsInvalidSetting() async throws { + @Test func routeSettingsInvalidSetting() async throws { try await assertRoute( - router: Router(), "meshtastic:///settings/invalidSetting", - NavigationState( - selectedTab: .settings - ) + NavigationState(selectedTab: .settings) ) } + // MARK: - navigateToNodeDetail + + @Test func navigateToNodeDetail() async { + let router = await Router() + await router.navigateToNodeDetail(nodeNum: 9876543210) + let state = await router.navigationState + #expect(state.selectedTab == .nodes) + #expect(state.nodeListSelectedNodeNum == 9876543210) + } + + // MARK: - State Transitions + + @Test func routingToNewTabClearsPreviousState() async throws { + let router = await Router() + + // First, route to messages with channel state + let messagesURL = try #require(URL(string: "meshtastic:///messages?channelId=1&messageId=100")) + await router.route(url: messagesURL) + let messagesState = await router.navigationState + #expect(messagesState.selectedTab == .messages) + #expect(messagesState.messages != nil) + + // Then route to map — messages state should remain but tab changes + let mapURL = try #require(URL(string: "meshtastic:///map?waypointId=42")) + await router.route(url: mapURL) + let mapState = await router.navigationState + #expect(mapState.selectedTab == .map) + #expect(mapState.map == .waypoint(42)) + } + + @Test func consecutiveRoutesUpdateState() async throws { + let router = await Router() + + let nodesURL = try #require(URL(string: "meshtastic:///nodes?nodenum=111")) + await router.route(url: nodesURL) + let first = await router.navigationState + #expect(first.selectedTab == .nodes) + #expect(first.nodeListSelectedNodeNum == 111) + + let nodesURL2 = try #require(URL(string: "meshtastic:///nodes?nodenum=222")) + await router.route(url: nodesURL2) + let second = await router.navigationState + #expect(second.selectedTab == .nodes) + #expect(second.nodeListSelectedNodeNum == 222) + } + + @Test func invalidSchemeDoesNotMutateExistingState() async throws { + let initial = NavigationState(selectedTab: .map, map: .waypoint(99)) + let router = await Router(navigationState: initial) + let badURL = try #require(URL(string: "https:///messages")) + await router.route(url: badURL) + let state = await router.navigationState + #expect(state == initial) + } + + // MARK: - Helpers + private func assertRoute( - router: Router, _ urlString: String, _ destination: NavigationState ) async throws { - let url = try XCTUnwrap(URL(string: urlString)) + let router = await Router() + let url = try #require(URL(string: urlString)) await router.route(url: url) let state = await router.navigationState - XCTAssertEqual(state, destination) + #expect(state == destination) } } From 68050e6fb8f21efa0376cdebd5125541439e8c9b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:42:59 -0700 Subject: [PATCH 2/7] Fix merge conflicts in PR #1614 (Spanish translations) (#1644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --- Localizable.xcstrings | 7144 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 7144 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 5f7a4fd9..1083039f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -20,6 +20,12 @@ "value" : "\t%@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "\t%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -61,6 +67,12 @@ "value" : "%@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -102,6 +114,12 @@ "value" : "%@%%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -213,6 +231,12 @@ "value" : "(Re)definer PIN_GPS_EN for dit printkort." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "(Re)define el PIN_GPS_EN para tu placa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -259,6 +283,12 @@ "value" : "%@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -306,6 +336,12 @@ "value" : "%1$@ - %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -359,6 +395,12 @@ "value" : "%1$@ - %2$@ - %3$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ - %3$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -464,6 +506,12 @@ "value" : "%@ - Keine Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Ninguna respuesta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -516,6 +564,12 @@ "value" : "%@ - Nicht gesendet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - No enviado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -568,6 +622,12 @@ "value" : "%1$@ (%2$@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ (%2$@)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -622,6 +682,12 @@ "value" : "%1$@ %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -675,6 +741,12 @@ "value" : "%1$@ %2$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -728,6 +800,12 @@ "value" : "%@ entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "a %@ de distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -780,6 +858,12 @@ "value" : "%@ kann bis zu %@ Byte lang sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ puede ser hasta %@ bytes de longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -826,6 +910,12 @@ "value" : "%@ kanaler?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Canales?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -866,6 +956,12 @@ }, "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ datos de configuración solicitados via PKC admin pero no se ha recibido respuesta desde el nodo remoto." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -888,6 +984,12 @@ "value" : "%@ dB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -940,6 +1042,12 @@ "value" : "%1$@, %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -993,6 +1101,12 @@ "value" : "%1$@: %2$lld / %3$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$lld / %3$lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1040,6 +1154,12 @@ "value" : "%@%%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1086,6 +1206,12 @@ "value" : "%@°F" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@°F" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1132,6 +1258,12 @@ "value" : "%@mA" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@mA" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1172,6 +1304,12 @@ "value" : "%@V" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@V" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1212,6 +1350,12 @@ "value" : "%d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1288,6 +1432,24 @@ } } }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Salto" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Saltos" + } + } + } + } + }, "it" : { "variations" : { "plural" : { @@ -1407,6 +1569,12 @@ "value" : "%d%%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1448,6 +1616,12 @@ "%f%%" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%f%%" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1471,6 +1645,12 @@ "value" : "%lf" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lf" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1517,6 +1697,12 @@ "value" : "%lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1750,12 @@ "value" : "%1$lld %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld %2$@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1616,6 +1808,24 @@ } } }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld feature" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld features" + } + } + } + } + }, "ru" : { "variations" : { "plural" : { @@ -1686,6 +1896,12 @@ "value" : "%lld oder weniger Hops entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld o menos saltos de distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1726,6 +1942,12 @@ "value" : "Samlet %lld aflæsninger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Lecturas Totales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1766,6 +1988,12 @@ "value" : "Samlet %lld detektioner" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Eventos de Detección Totales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1806,6 +2034,12 @@ "value" : "%lld%%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1858,6 +2092,12 @@ "value" : "%llddb Übertragungsleistung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb Potencia de Transmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1953,6 +2193,12 @@ "value" : "< 1%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "< 1%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1995,6 +2241,12 @@ "comment" : "A warning label below the picker, indicating that the selected update interval is not one of the optimized options.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️ El valor configurado: (%@) no es una de las opciones optimizadas." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -2011,6 +2263,12 @@ "value" : "🦕 Ikke-supporteret version 🦖 ☄️" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "🦕 Versión End of life 🦖 ☄️" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2046,6 +2304,12 @@ "0" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2057,6 +2321,12 @@ }, "1" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2074,6 +2344,12 @@ "value" : "1 byte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 byte" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2120,6 +2396,12 @@ "value" : "1 hop væk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "a 1 salto de distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2167,6 +2449,12 @@ "value" : "2.4 GHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "2.4 Ghz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2207,6 +2495,12 @@ "value" : "7" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "7" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2247,6 +2541,12 @@ }, "12 Hour Clock" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reloj 12 Horas" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2275,6 +2575,12 @@ "value" : "25" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "25" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2321,6 +2627,12 @@ "value" : "50" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "50" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2367,6 +2679,12 @@ "value" : "75" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "75" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2413,6 +2731,12 @@ "value" : "100" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2459,6 +2783,12 @@ "value" : "128 bit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "128 bit" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2499,6 +2829,12 @@ }, "180" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "180" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2516,6 +2852,12 @@ "value" : "256 bit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "256 bit" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2560,6 +2902,12 @@ }, "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El índice de canal 0 indica el canal principal desde donde se envían los paquetes de broadcast. Los datos de ubicación se transmiten desde el primer canal donde esté habilitado con el firmware 2.7 en adelante." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -2577,6 +2925,12 @@ "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {}, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado verde significa que el canal está encriptado de forma segura con una clave AES 128 o 256 bit." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2611,6 +2965,12 @@ "value" : "In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un código QR Meshtastic contiene la configuración LoRa y los valores de los canales necesarios para la comunicación radio. Puedes compartir la configuración de canales completa usando la opción Reemplazar Canales, si seleccionas Agregar Canales tus canales compartidos serán añadidos a los canales existentes en la radio receptora." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -2675,6 +3035,12 @@ }, "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado rojo abierto significa que el canal no está encriptado de forma segura y se usa para datos de ubicación precisos, que no tiene clave o que tiene una clave de 1 byte conocida." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -2691,6 +3057,12 @@ }, "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado rojo abierto con un warning significa que el canal no está encriptado de forma segura y se usa para datos de ubicación precisos que están siendo subidos a internet via MQTT, que no tiene clave o que tiene una clave de 1 byte conocida." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -2713,6 +3085,12 @@ "value" : "Der er igangsat en rutesporing (trace route), men der er ikke modtaget svar." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un Trace Route fue enviado, no se ha recibido respuesta." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2748,6 +3126,12 @@ "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado amarillo abierto significa que el canal no está encriptado de forma segura pero no se usa para datos de ubicación precisos, que no tiene clave o que tiene una clave de 1 byte conocida." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2760,6 +3144,12 @@ "comment" : "A description of a yellow open lock in the Channels Help view.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado amarillo abierto significa que el canal no está encriptado de forma segura pero no se usa para datos de ubicación precisos, que no tiene clave o que tiene una clave de 1 byte conocida." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -2782,6 +3172,12 @@ "value" : "Über Meshtastic" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2828,6 +3224,12 @@ "value" : "Über Meshtastic" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de Meshtastic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2874,6 +3276,12 @@ "value" : "Genauigkeit %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precisión %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2914,6 +3322,12 @@ "value" : "Ack SNR: %@ dB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ack SNR: %@ dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2954,6 +3368,12 @@ "value" : "Bekræftelsestidspunkt: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo ACK: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3001,6 +3421,12 @@ "value" : "Bestätigt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3071,6 +3497,12 @@ "value" : "Modtagelse bekræftet af en anden node" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmado por otro nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3117,6 +3549,12 @@ "value" : "Aktionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3163,6 +3601,12 @@ "value" : "Aktiv" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3209,6 +3653,12 @@ "value" : "Aktivität" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actividad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3261,6 +3711,12 @@ "value" : "ADC Override" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ADC Override" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -3320,6 +3776,12 @@ "value" : "Tilføj kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar Canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3360,6 +3822,12 @@ "value" : "Tilføj kanaler" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar Canales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3394,6 +3862,12 @@ }, "Add Contact" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar Contacto" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -3422,6 +3896,12 @@ }, "Add Meshtastic Node %@ as a contact" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar nodo Meshtastic %@ como contacto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3468,6 +3948,12 @@ "value" : "Zu Favoriten hinzufügen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar a favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3508,6 +3994,12 @@ "value" : "Yderligere hjælp" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda adicional" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3554,6 +4046,12 @@ "value" : "Adresse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3594,6 +4092,12 @@ }, "Admin Keys" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claves de Admin" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -3622,6 +4126,12 @@ "value" : "Administration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3668,6 +4178,12 @@ "value" : "Administration aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración habilitada" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -3702,6 +4218,12 @@ "value" : "Avanceret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3748,6 +4270,12 @@ "value" : "Avanceret indbygget GPS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo GPS Avanzado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3795,6 +4323,12 @@ "value" : "Avancerede GPIO-indstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones GPIO avanzadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3835,6 +4369,12 @@ "value" : "Avancerede positionsflag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flags de posición avanzadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3888,6 +4428,12 @@ "value" : "Nach" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3964,6 +4510,24 @@ } } }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después de %lld Día" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "Después de %lld Días" + } + } + } + } + }, "ja" : { "variations" : { "plural" : { @@ -4046,6 +4610,12 @@ "value" : "Nach dem Ändern der Einstellungen wird das Gerät neu starten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después de guardar los valores de configuración el nodo se reseteará." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4123,6 +4693,12 @@ "value" : "Nachmittag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarde" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4169,6 +4745,12 @@ "value" : "Airtime" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Airtime" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4239,6 +4821,12 @@ "value" : "Alarm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4279,6 +4867,12 @@ "value" : "Udløs GPIO-sirene ved modtagelse af en ASCII-klokke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso GPIO buzzer cuando se recibe una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4325,6 +4919,12 @@ "value" : "Advarsel GPIO-vibrator ved modtagelse af en besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta GPIO buzzer cuando se recibe un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4365,6 +4965,12 @@ "value" : "Advarsel GPIO-vibrator ved modtagelse af en ASCII-klokke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta GPIO vibra motor cuando se recibe una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4411,6 +5017,12 @@ "value" : "Advarsel GPIO-vibrator ved modtagelse af en besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta GPIO vibra motor cuando se recibe un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4451,6 +5063,12 @@ "value" : "Giv besked ved modtagelse af en ASCII-klokke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta cuando se recibe una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4497,6 +5115,12 @@ "value" : "Giv besked ved modtagelse af en besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta cuando se recibe un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4543,6 +5167,12 @@ "value" : "Alle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4589,6 +5219,12 @@ "value" : "Tillad positions-anmodninger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir Peticiones de Posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4629,6 +5265,12 @@ "value" : "Højde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4675,6 +5317,12 @@ "value" : "Höhe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4721,6 +5369,12 @@ "value" : "Höhe %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altitud %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4761,6 +5415,12 @@ "value" : "Geoidhøjde Adskillelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Separación Geoidal de Altitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4801,6 +5461,12 @@ "value" : "Højde er middelhavsniveau" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altitud es nivel medio del mar (MSL)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4848,6 +5514,12 @@ "value" : "Immer an" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre encendido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4924,6 +5596,12 @@ "value" : "Immer nach Norden zeigen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre apuntar al norte" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4970,6 +5648,12 @@ "value" : "Ambientebeleuchtung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luz ambiente" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5040,6 +5724,12 @@ "value" : "Ambientebeleuchtungskonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración luz ambiente" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5105,6 +5795,12 @@ "value" : "Konfiguration af ambient belysningsmodul modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de luz ambiante recibida : %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5181,6 +5877,12 @@ "value" : "Ein quelloffenes, netzunabhängiges, dezentrales Mesh-Netzwerk, das auf kostengünstigen, stromsparenden Funkgeräten läuft." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Una red mesh open source, off-grid, descentralizada, que funciona con radios de bajo coste y de baja potencia." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5227,6 +5929,12 @@ "value" : "Alle missede beskeder vil blive leveret igen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cualquier mensaje perdido será entregado nuevamente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5280,6 +5988,12 @@ "value" : "Client (Standard) - Mit App verbundener Client." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicación conectada o dispositivo de mensajería aislado." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5356,6 +6070,12 @@ "value" : "App-Daten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5402,6 +6122,12 @@ "value" : "App-filer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5442,6 +6168,12 @@ }, "App Icon" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icono de la App" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -5464,6 +6196,12 @@ "value" : "Mitteilungseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones de la App" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -5492,6 +6230,12 @@ "value" : "App-Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes de la App" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5590,6 +6334,12 @@ "value" : "Ungefährer Standort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición Aproximada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5630,6 +6380,12 @@ "value" : "Er du sikker på, at du vil slette denne besked?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar este mensaje?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5682,6 +6438,12 @@ "value" : "Bist du sicher dass du den Knoten auf die Werkseinstellungen zurücksetzen willst?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar el nodo a valores de fábrica?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5734,6 +6496,12 @@ "value" : "Bist Du sicher?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5805,6 +6573,12 @@ "value" : "Australien og New Zealand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Australia / Nueva Zelanda" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5839,6 +6613,12 @@ }, "Automatically Connect" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conexión Automática" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -5861,6 +6641,12 @@ "value" : "Skifter automatisk til den næste side på skærmen som en karrusel, baseret på det angivne interval." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia automáticamente a la siguiente página en pantalla, como un carrusel, según el intervalo especificado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5913,6 +6699,12 @@ "value" : "Verfügbare Modem-Voreinstellungen, Standard ist „Long Range - Fast“." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presets del modem disponibles, por defecto es Long Fast.“" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5965,6 +6757,12 @@ "value" : "Geräte in der Nähe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radios Disponibles" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6042,6 +6840,12 @@ "value" : "Zurück" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atrás" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6128,6 +6932,12 @@ }, "Backup your private key to your iCloud keychain." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Respalda tu clave privada en el llavero de iCloud." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -6157,6 +6967,12 @@ "value" : "Dårlig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Incorrecto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6198,6 +7014,12 @@ "value" : "Fejl i forespørgsel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Petición incorrecta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6274,6 +7096,12 @@ "value" : "Bandbreite" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ancho de banda" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6321,6 +7149,12 @@ "value" : "Søjle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6367,6 +7201,12 @@ "value" : "Søjlediagramserie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie Bar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6414,6 +7254,12 @@ "value" : "Barometertryk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presión Barométrica" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6466,6 +7312,12 @@ "value" : "Batterie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batería" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -6537,6 +7389,12 @@ "value" : "Batterie Ladung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel de Batería" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6614,6 +7472,12 @@ "value" : "Batterie Ladung %" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel de Batería %" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6685,6 +7549,12 @@ "value" : "Batterie Ladung %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel de Batería %d" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6755,6 +7625,12 @@ "value" : "Baud" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baudios" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6828,6 +7704,12 @@ "value" : "Biken" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En bicicleta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6874,6 +7756,12 @@ "value" : "BLE" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7002,6 +7890,12 @@ "value" : "Bluetooth" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7078,6 +7972,12 @@ "value" : "Bluetooth Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración Bluetooth" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7155,6 +8055,12 @@ "value" : "Bluetooth Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración Bluetooth recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7221,6 +8127,12 @@ "comment" : "A heading displayed on a view that guides users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectividad Bluetooth" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -7269,6 +8181,12 @@ }, "Broadcast Device Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de Transmisión del Dispositivo" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -7286,6 +8204,12 @@ "value" : "Broadcast-interval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7339,6 +8263,12 @@ "value" : "Sendet GPS-Positionspakete mit Priorität." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmitir paquetes de posición GPS con prioridad." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7416,6 +8346,12 @@ "value" : "Sendet den Standort regelmäßig als Nachricht an den Standardkanal, um die Suche nach dem Gerät zu unterstützen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmitir la posición como un mensaje al canal por defecto regularmente para ayudar con la recuperación del dispositivo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7493,6 +8429,12 @@ "value" : "Sendet Telemetriepakete mit Priorität." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmitir los paquetes de telemetría con prioridad." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7563,6 +8505,12 @@ "value" : "GPIO-knap" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Botón GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7609,6 +8557,12 @@ "value" : "Køb komplette radioer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprar Radios Completas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7655,6 +8609,12 @@ "value" : "GPIO-vibrator" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zumbador GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7701,6 +8661,12 @@ "value" : "Ved at aktivere denne funktion anerkender du og giver udtrykkeligt samtykke til overførsel af din enheds geolokation i realtid over MQTT-protokollen uden kryptering. Disse positionsdata kan bruges til formål som livekortrapportering, enhedssporing og relaterede telemetriefunktioner." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al habilitar esta función, aceptas expresamente la transmisión de tu ubicación geográfica en tiempo real de tu dispositivo mediante el protocolo MQTT sin encriptar. Estos datos de ubicación se pueden usar para propósitvos como reporte en mapa en tiempo real, localización de dispositivo y otras funciones de telemetría asociadas." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -7742,6 +8708,12 @@ "value" : "Bytes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bytes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7807,6 +8779,12 @@ "Bytes Used" : { "comment" : "VoiceOver value for bytes used", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bytes Usados" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -7841,6 +8819,12 @@ "value" : "Rufzeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Señal de llamada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7893,6 +8877,12 @@ "value" : "Das Rufzeichen darf nicht leer sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La señal de llamada no debe estar vacía." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7945,6 +8935,12 @@ "value" : "Abbrechen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8016,6 +9012,12 @@ "value" : "Konfigurationsmodul for standardbesked modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuraciíon del módulo Canned Message recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8092,6 +9094,12 @@ "value" : "Canned Messages" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canned Messages" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8168,6 +9176,12 @@ "value" : "Canned Messages Config" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de Canned Messages" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8239,6 +9253,12 @@ "value" : "Modtagne beskeder for: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibidos Canned Messages para : %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8309,6 +9329,12 @@ "value" : "Karusselinterval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo del carrusel" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8355,6 +9381,12 @@ "value" : "Kategorien" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Categorías" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8407,6 +9439,12 @@ "value" : "Kategorie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Categoría" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8447,6 +9485,12 @@ "value" : "Ch1 strøm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corriente Ch1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8487,6 +9531,12 @@ "value" : "Ch1 spænding" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8527,6 +9577,12 @@ "value" : "Ch2 strøm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corriente Ch2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8567,6 +9623,12 @@ "value" : "Ch2 spænding" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8607,6 +9669,12 @@ "value" : "Ch3 strøm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corriente Ch3" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8647,6 +9715,12 @@ "value" : "Ch3 spænding" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch3" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8693,6 +9767,12 @@ "value" : "Kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8763,6 +9843,12 @@ "value" : "Kanal 0 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 0 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8809,6 +9895,12 @@ "value" : "Kanal 1" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8849,6 +9941,12 @@ "value" : "Kanal 1 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 1 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8895,6 +9993,12 @@ "value" : "Kanal 2" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8935,6 +10039,12 @@ "value" : "Kanal 2 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 2 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8981,6 +10091,12 @@ "value" : "Kanal 3" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 3" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9021,6 +10137,12 @@ "value" : "Kanal 3 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 3 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9067,6 +10189,12 @@ "value" : "Kanal 4 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 4 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9113,6 +10241,12 @@ "value" : "Kanal 5 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 5 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9159,6 +10293,12 @@ "value" : "Kanal 6 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 6 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9205,6 +10345,12 @@ "value" : "Kanal 7 inkluderet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 7 Incluído" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9245,6 +10391,12 @@ }, "Channel Details" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del Canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9291,6 +10443,12 @@ "value" : "Kanalnavn" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre del Canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9337,6 +10495,12 @@ "value" : "Kanalnummeret skal være mellem 0 og 7." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El número del canal debe estar entre 0 y 7." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9383,6 +10547,12 @@ "value" : "Kanalrolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rol del Canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9429,6 +10599,12 @@ "value" : "Kanal-URL" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9475,6 +10651,12 @@ "value" : "Kanalbelegung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilización del Canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9545,6 +10727,12 @@ "value" : "Kanaludnyttelsesgrad %@%%" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilización del canal %@%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9591,6 +10779,12 @@ "value" : "Kanäle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canales" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9662,6 +10856,12 @@ "value" : "Kanaler tilføjet fra QR-koden blev ikke gemt. Når kanaler tilføjes, skal navnene være unikke." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los canales añadidos desde el código QR no se guardaron. Cuando se añaden canales los nombres deben ser únicos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9702,6 +10902,12 @@ }, "Channels Help" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda de los Canales" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -9731,6 +10937,12 @@ "value" : "Graf" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gráfico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9777,6 +10989,12 @@ "value" : "ÆND" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "CHG" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9818,6 +11036,12 @@ "value" : "Kina" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "China" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9875,6 +11099,12 @@ "value" : "Tøm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpiar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9927,6 +11157,12 @@ "value" : "App-Daten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar Datos de Aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9997,6 +11233,12 @@ "value" : "Tøm log" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar Log" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10037,6 +11279,12 @@ "value" : "Veraltete Knoten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar nodos obsoletos" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -10066,6 +11314,12 @@ "value" : "Klient" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10108,6 +11362,12 @@ "comment" : "A message displayed in a confirmation dialog when trying to favorite a node as a CLIENT_BASE.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente Base solo debe tener como favoritos otros nodos bajo tu control. El uso inapropiado dañará tu mesh local." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10133,6 +11393,12 @@ "value" : "Client - Versteckt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente Oculto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10179,6 +11445,12 @@ "value" : "Klienthistorik" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historial del cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10225,6 +11497,12 @@ "value" : "Klienthistorik-anmodning sendt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Petición de cronología del cliente enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10272,6 +11550,12 @@ "value" : "Tavs klient (client mute)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente Mudo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10318,6 +11602,12 @@ "value" : "Klientindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones del cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10364,6 +11654,12 @@ "value" : "Med uret roterende hændelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento de giro en sentido horario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10416,6 +11712,12 @@ "value" : "Schließen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10486,6 +11788,12 @@ "value" : "Kodningshastighed" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tasa de codificación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10538,6 +11846,12 @@ "value" : "Farbe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Color" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10584,6 +11898,12 @@ "value" : "Bleibe mit deinen Freunden und deiner Community in Verbindung, auch abseits vom Mobilfunknetz." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comunícate con tus amigos y tu comunidad fuera de la red móvil." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10606,6 +11926,12 @@ "value" : "Kommunikerer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En comunicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10646,6 +11972,12 @@ "value" : "Support fra fællesskabet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soporte de la comunidad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10680,6 +12012,12 @@ }, "Compass" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brújula" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10696,6 +12034,12 @@ "value" : "Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10749,6 +12093,12 @@ "value" : "Konfiguration für: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración para: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10789,6 +12139,12 @@ "value" : "Standardkonfigurationer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presets de Configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10841,6 +12197,12 @@ "value" : "Konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10877,6 +12239,12 @@ "comment" : "Button label to guide users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar la conectividad Bluetooth" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10895,6 +12263,12 @@ "comment" : "Button label to configure local network access permissions.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar acceso a la red local" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10917,6 +12291,12 @@ "value" : "Standortberechtigungen konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar permisos de ubicación" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10939,6 +12319,12 @@ "value" : "Mitteilungsberechtigungen konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar permisos de notificaciones" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -10961,6 +12347,12 @@ "value" : "Bekræft" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10995,6 +12387,12 @@ }, "Connect" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectar" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -11023,6 +12421,12 @@ "value" : "Verbunden mit einem Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectar a un nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11063,6 +12467,12 @@ "value" : "Tilslut MQTT over proxy" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectar a MQTT via Proxy" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11097,6 +12507,12 @@ "value" : "Tilslut ny radio" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Conectar a la nueva radio?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11143,6 +12559,12 @@ "value" : "Derzeit verbunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11219,6 +12641,12 @@ "value" : "Verbunden mit Knoten %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo conectado %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11305,6 +12733,12 @@ "value" : "Verbinde..." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectando . ." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11375,6 +12809,12 @@ "value" : "Hvis du tilslutter en ny radio bliver all appens data på telefonen slettet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectarse a una nueva radio borrará todos los datos de la app en el teléfono." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11421,6 +12861,12 @@ "value" : "Verbindungsversuch %lld von 10" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intento de conexión %lld de 10" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11461,6 +12907,12 @@ }, "Connection Name" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de la conexión" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -11483,6 +12935,12 @@ "value" : "Samtykke til at dele ukrypterede node-data via MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consentir compartir datos del nodo sin cifrar mediante MQTT " + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11518,6 +12976,12 @@ "value" : "Kontaktfilter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtros de contacto" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -11534,6 +12998,12 @@ }, "Contact URL" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL del contacto" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11575,6 +13045,12 @@ "value" : "Kontakte (%@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contactos (%@)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11645,6 +13121,12 @@ "value" : "Kontroltype" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo de control" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11691,6 +13173,12 @@ "value" : "Styrer den blinkende LED på enheden. For de fleste enheder vil dette styre en af de op til 4 LED'er, oplader- og GPS-LED'er kan ikke styres." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Controla el parpadeo del LED del dispositivo. Para la mayoría de los dispositivos, esto controlará uno de los 4 LEDs, los LED del cargador y GPS no son controlables." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11743,6 +13231,12 @@ "value" : "Konvexe Hülle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoltura convexa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11789,6 +13283,12 @@ "value" : "Koordinate" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordenar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11835,6 +13335,12 @@ "value" : "Koordinate %1$@, %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordenadas %1$@, %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11887,6 +13393,12 @@ "value" : "Koordinaten:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordenadas:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11933,6 +13445,12 @@ "value" : "Kopieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12009,6 +13527,12 @@ "value" : "Knoten nicht gefunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo no encontrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12055,6 +13579,12 @@ "value" : "Mod-uret Rundt Roterende Begivenhed" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento rotativo antihorario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12101,6 +13631,12 @@ "value" : "Wegpunkt erstellen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear punto de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12141,6 +13677,12 @@ "value" : "Erstelle deine eigenen Netzwerke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea tus propias redes" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -12169,6 +13711,12 @@ "value" : "Erstellt: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Creado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12209,6 +13757,12 @@ "value" : "Kritische Hinweise" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alertas críticas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -12231,6 +13785,12 @@ "value" : "Strøm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actual" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12277,6 +13837,12 @@ "value" : "Aktuelle Firmware Version: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware actual: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12329,6 +13895,12 @@ "value" : "Aktuelle Firmware Version: %1$@, neuste Firmware Version %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware actual: %@, última versión de firmware: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12381,6 +13953,12 @@ "value" : "Aktuell: %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actual: %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12421,6 +13999,12 @@ "value" : "I øjeblikket er den anbefalede måde at opdatere ESP32-enheder på at bruge web-flasheren på en stationær computer fra en Chrome-baseret browser. Det fungerer ikke på mobile enheder eller over BLE." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualmente, la forma recomendada de actualizar dispositivos ESP32 es utilizar el flash web en una computadora de escritorio desde un navegador basado en Chrome. No funciona en dispositivos móviles ni a través de BLE." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12473,6 +14057,12 @@ "value" : "Datum" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fecha" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12513,6 +14103,12 @@ "value" : "Fejlfinding" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Depurar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12565,6 +14161,12 @@ "value" : "Fehlersuchprotokolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros de depuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12605,6 +14207,12 @@ "value" : "Fejlfindingslogs %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros de depuración%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12651,6 +14259,12 @@ "value" : "Standard" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predeterminado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12722,6 +14336,12 @@ "value" : "Standardskærmlayout på 128x64" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diseño de pantalla predeterminado de 128x64" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12774,6 +14394,12 @@ "value" : "Löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12839,6 +14465,12 @@ "Delete All" : {}, "Delete all config, keys and BLE bonds? " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las configuraciones, claves y enlaces BLE? " + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12867,6 +14499,12 @@ }, "Delete all config? " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las configuraciones? " + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12901,6 +14539,12 @@ "value" : "Slet alle enhedens måledata?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las métricas del dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12971,6 +14615,12 @@ "value" : "Slet alle miljødata?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las métricas del entorno?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13017,6 +14667,12 @@ "value" : "Slet alle persontællingsdata?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todos los datos de los pasajeros?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13063,6 +14719,12 @@ "value" : "Slet alle positioner?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las posiciones?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13103,6 +14765,12 @@ "value" : "Slet besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13143,6 +14811,12 @@ "value" : "Slet beskeder" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar mensajes" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13189,6 +14863,12 @@ "value" : "Knoten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13235,6 +14915,12 @@ "value" : "Knoten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13275,6 +14961,12 @@ "value" : "Slet alle energiforbrugsdata?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar métricas de potencia?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13321,6 +15013,12 @@ "value" : "Beschreibung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descripción" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13361,6 +15059,12 @@ "value" : "Beskrivelsen skal være under 100 bytes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La descripción debe tener menos de 100 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13401,6 +15105,12 @@ }, "Details..." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles..." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -13423,6 +15133,12 @@ "value" : "Detektion" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13463,6 +15179,12 @@ "value" : "Detektionshændelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13510,6 +15232,12 @@ "value" : "Detection Sensor" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensor de detección" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13580,6 +15308,12 @@ "value" : "Detektionssensor-indstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del sensor de detección" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13644,6 +15378,12 @@ "value" : "Detektionssensor-log" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro del sensor de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13684,6 +15424,12 @@ "value" : "Registreringssensorbeskeder modtages som tekstbeskeder. Hvis du aktiverer meddelelser, vil du modtage en meddelelse for hver registreringsbesked, der modtages, samt et tilsvarende badge for ulæste beskeder." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes del sensor de detección se reciben como mensajes de texto. Si habilita las notificaciones, recibirá una notificación por cada mensaje de detección recibido y la correspondiente insignia de mensaje no leído." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13731,6 +15477,12 @@ "value" : "Registrering af sensors modulkonfiguration modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo del sensor de detección recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13801,6 +15553,12 @@ "value" : "Udviklere" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrolladores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13853,6 +15611,12 @@ "value" : "Gerät" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13929,6 +15693,12 @@ "value" : "Gerätekonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14006,6 +15776,12 @@ "value" : "Gerätekonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14082,6 +15858,12 @@ "value" : "Gerätekonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14140,6 +15922,12 @@ "value" : "Geräte-GPS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo GPS" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14186,6 +15974,12 @@ "value" : "Enheden administreres af en mesh-administrator, brugeren har ikke adgang til enhedens indstillinger." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El dispositivo es administrado por un administrador de malla, el usuario no puede acceder a ninguna de las configuraciones del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14239,6 +16033,12 @@ "value" : "Device Metadata empfangen von: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metadatos del dispositivo recibidos de: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14310,6 +16110,12 @@ "value" : "Enhedsmåledata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14356,6 +16162,12 @@ "value" : "Enhedsmetriklog" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14408,6 +16220,12 @@ "value" : "Gerätemodell: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modelo de dispositivo: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14448,6 +16266,12 @@ }, "Device Options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones del dispositivo" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -14464,6 +16288,12 @@ "value" : "Enhedsrolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Función del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14510,6 +16340,12 @@ "value" : "Enhedsskærm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14563,6 +16399,12 @@ "value" : "Gerät, das keine Pakete von anderen Geräten weiterleitet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo que no reenvía paquetes desde otros dispositivos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14640,6 +16482,12 @@ "value" : "Gerät, das nur bei Bedarf sendet, um nicht entdeckt zu werden oder Strom zu sparen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo que solo transmite según sea necesario para sigilo o ahorro de energía." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14710,6 +16558,12 @@ "value" : "Standard PDOP bruges som udgangspunkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dilución de precisión (DOP) PDOP utilizado por defecto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14756,6 +16610,12 @@ "value" : "Direkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "directo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14808,6 +16668,12 @@ "value" : "Hilfe für Direktnachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda por mensaje directo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14848,6 +16714,12 @@ }, "Direct Message Key" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tecla de mensaje directo" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -14882,6 +16754,12 @@ "value" : "Direktnachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes directos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14952,6 +16830,12 @@ "value" : "Direkte beskeder bruger den nye public key-infrastruktur til kryptering. Kræver firmware-version 2.5 eller nyere" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes directos utilizan la nueva infraestructura de clave pública para el cifrado. Requiere versión de firmware 2.5 o superior." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14998,6 +16882,12 @@ "value" : "Direkte beskeder bruger den fælles krypteringsnøgle for kanalen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes directos utilizan la clave compartida del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15050,6 +16940,12 @@ "value" : "Deaktiviert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Discapacitado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15126,6 +17022,12 @@ "value" : "Trennen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15190,6 +17092,12 @@ }, "Disconnect Node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar nodo" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -15212,6 +17120,12 @@ }, "Disconnect the currently connected node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar el nodo actualmente conectado" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -15246,6 +17160,12 @@ "value" : "Tastatur ausblenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descartar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15322,6 +17242,12 @@ "value" : "Display (Device Screen)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15392,6 +17318,12 @@ "value" : "Skærmopsætning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de pantalla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15469,6 +17401,12 @@ "value" : "Display Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de pantalla recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15539,6 +17477,12 @@ "value" : "Vis Fahrenheit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar grados Fahrenheit" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15585,6 +17529,12 @@ "value" : "Display Mode" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de visualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15631,6 +17581,12 @@ "value" : "Darstellung der Entfernung zwischen deinem Handy und anderen Meshtastic-Knoten mit Positionsangabe." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muestra la distancia entre tu teléfono y otros nodos Meshtastic con posiciones." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -15653,6 +17609,12 @@ "value" : "Display Units" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidades de visualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15705,6 +17667,12 @@ "value" : "Distanz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15751,6 +17719,12 @@ "value" : "Distanzfilter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtros de distancia" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -15773,6 +17747,12 @@ "value" : "Distanzmessungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediciones de distancia" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -15789,6 +17769,12 @@ }, "Distance: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distancia: %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -15811,6 +17797,12 @@ "value" : "Dokumentation" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15851,6 +17843,12 @@ }, "Done" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "hecho" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -15885,6 +17883,12 @@ "value" : "Dobbelttryk som knap" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque dos veces como botón" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15938,6 +17942,12 @@ "value" : "Runter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "abajo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16008,6 +18018,12 @@ "value" : "Downlink slået til" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enlace descendente habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16055,6 +18071,12 @@ "value" : "Træk-og-slip firmwareopdatering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización de firmware de arrastrar y soltar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16101,6 +18123,12 @@ "value" : "Træk-og-slip firmwareopdateringsdokumentation" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentación de actualización de firmware de arrastrar y soltar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16147,6 +18175,12 @@ "value" : "Træk og slip er den anbefalede måde at opdatere firmware til NRF-enheder. Hvis din iPhone eller iPad har USB-C, vil det fungere med dit almindelige USB-C-opladerkabel, for Lightning-enheder har du brug for Apple Lightning til USB-kameraadapter." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arrastrar y soltar es la forma recomendada de actualizar el firmware para dispositivos NRF. Si su iPhone o iPad es USB-C, funcionará con su cable de carga USB-C habitual; para dispositivos Lightning, necesita el adaptador de cámara Lightning a USB de Apple." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16200,6 +18234,12 @@ "value" : "Fahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conducir" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16246,6 +18286,12 @@ "value" : "Placer nål i kort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colocar pin en mapas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16286,6 +18332,12 @@ "value" : "Richte einfach private Mesh-Netzwerke für eine sichere und zuverlässige Kommunikation in abgelegenen Gebieten ein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure fácilmente redes de malla privadas para una comunicación segura y confiable en áreas remotas." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -16308,6 +18360,12 @@ "value" : "Echo" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "eco" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16372,6 +18430,12 @@ "value" : "Redigerer viapunkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Editar punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16419,6 +18483,12 @@ "value" : "Achtzehn Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieciocho horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16495,6 +18565,12 @@ "value" : "Höhenunterschied" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elev. Ganar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16535,6 +18611,12 @@ "value" : "Emoji" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "emojis" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16581,6 +18663,12 @@ "value" : "Tom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "vacio" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16621,6 +18709,12 @@ }, "Enable broadcasting device metrics to the mesh network. When disabled, metrics are only sent to connected clients." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite la transmisión de métricas de dispositivos a la red de malla. Cuando está deshabilitado, las métricas solo se envían a los clientes conectados." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -16637,6 +18731,12 @@ "value" : "Aktiver udsendelse af pakker via UDP over det lokale netværk." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite la transmisión de paquetes a través de UDP a través de la red local." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16677,6 +18777,12 @@ "value" : "Standortfreigabe aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar compartir ubicación" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -16699,6 +18805,12 @@ "value" : "Tillad notifikationer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar notificaciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16746,6 +18858,12 @@ "value" : "Aktivér denne enhed som en Store and Forward-server. Kræver en ESP32-enhed med PSRAM." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite este dispositivo como servidor Store and Forward. Requiere un dispositivo ESP32 con PSRAM." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16792,6 +18910,12 @@ "value" : "Aktiviert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16869,6 +18993,12 @@ "value" : "Aktiviert automatische TAK-PLI-Übertragungen und verringert die Anzahl der Routineübertragungen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite transmisiones automáticas de TAK PLI y reduce las transmisiones de rutina." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16939,6 +19069,12 @@ "value" : "Aktiverer enheder med native I2S-lydudgang til at bruge RTTTL over højttaler som en buzzer. T-Watch S3 og T-Deck har for eksempel denne kapabilitet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite que los dispositivos con salida de audio I2S nativa utilicen el RTTTL a través del altavoz como un timbre. T-Watch S3 y T-Deck, por ejemplo, tienen esta capacidad." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16985,6 +19121,12 @@ "value" : "Aktiviert den blauen Standort-Punkt für dein Handy in der Mesh-Karte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el punto de ubicación azul para su teléfono en el mapa de malla." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -17007,6 +19149,12 @@ "value" : "Aktiverer detektionssensormodulet. Det skal være aktiveret både på noden med sensoren og på alle noder, hvor du ønsker at modtage detektionssensor-tekstbeskeder eller se detektionssensorloggen og diagrammet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el módulo del sensor de detección; debe estar habilitado tanto en el nodo con el sensor como en cualquier nodo en el que desee recibir mensajes de texto del sensor de detección o ver el registro y el gráfico del sensor de detección." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17053,6 +19201,12 @@ "value" : "Aktiverer butiks- og videresendelsesmodulet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el módulo de almacenamiento y reenvío." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17093,6 +19247,12 @@ "value" : "Aktivering af Ethernet vil deaktivere bluetooth-forbindelsen til appen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al habilitar Ethernet se deshabilitará la conexión bluetooth a la aplicación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17127,6 +19287,12 @@ "value" : "Aktivering af WiFi vil deaktivere Bluetooth-forbindelsen til appen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar WiFi deshabilitará la conexión bluetooth a la aplicación." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -17149,6 +19315,12 @@ "value" : "Encoder trykhændelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento de prensa del codificador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17195,6 +19367,12 @@ "value" : "Verschlüsselt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cifrado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17272,6 +19450,12 @@ "value" : "Verschlüsseltes Senden fehlgeschlagen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de envío cifrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17306,6 +19490,12 @@ "value" : "Kryptering aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cifrado habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17358,6 +19548,12 @@ "value" : "DFÜ-Modus aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese al modo DFU" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17400,6 +19596,12 @@ "comment" : "A label for a text field where the user can enter a hostname or IP address and optionally a port number.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduzca el nombre de host[:puerto]" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -17430,6 +19632,12 @@ "value" : "Umgebung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "medio ambiente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17482,6 +19690,12 @@ "value" : "Umgebung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medio ambiente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17529,6 +19743,12 @@ "value" : "Miljødata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17569,6 +19789,12 @@ }, "Environment Metrics Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de entorno habilitadas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -17585,6 +19811,12 @@ "value" : "Miljødata-log" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17625,6 +19857,12 @@ }, "Environment Sensor Options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores ambientales" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -17647,6 +19885,12 @@ "value" : "Alle App-Daten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar todos los datos de la aplicación?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17699,6 +19943,12 @@ "value" : "Alle Geräte- und App-Daten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar todos los datos del dispositivo y de las aplicaciones?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17745,6 +19995,12 @@ "value" : "Fejl: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17791,6 +20047,12 @@ "value" : "ESP 32 OTA-opdatering er et igangværende arbejde, klik på knappen nedenfor for at sende din enhed en genstart til ota admin-besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La actualización de ESP 32 OTA es un trabajo en progreso, haga clic en el botón a continuación para enviar su dispositivo a un reinicio en el mensaje de administrador de ota." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17837,6 +20099,12 @@ "value" : "ESP32-enhedens firmwareopdatering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización del firmware del dispositivo ESP32" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17883,6 +20151,12 @@ "value" : "Ethernet-indstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de Ethernet" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17924,6 +20198,12 @@ "value" : "EU 433 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unión Europea 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17965,6 +20245,12 @@ "value" : "EU 868 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unión Europea 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18012,6 +20298,12 @@ "value" : "Abend" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tarde" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18052,6 +20344,12 @@ "value" : "Byt Positioner" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones de intercambio" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18086,6 +20384,12 @@ }, "Exchange User Info" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Información de usuario de Exchange" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -18109,6 +20413,12 @@ "value" : "Ausrufezeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "exclamación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18173,6 +20483,12 @@ }, "Expiration" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caducidad" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18207,6 +20523,12 @@ "value" : "Zeitpunkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caducar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18253,6 +20575,12 @@ "value" : "Automatisches Löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vence" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18293,6 +20621,12 @@ "value" : "Udløber: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expira: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18339,6 +20673,12 @@ "value" : "Exportieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18385,6 +20725,12 @@ "value" : "Externe Benachrichtigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificación externa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18461,6 +20807,12 @@ "value" : "Einstellungen der externen Benachrichtigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de notificación externa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18532,6 +20884,12 @@ "value" : "Moduletilkonfiguration for ekstern meddelelse modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de notificación externa recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18608,6 +20966,12 @@ "value" : "Werkseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecimiento de fábrica" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18642,6 +21006,12 @@ }, "Factory reset will delete device and app data." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El restablecimiento de fábrica eliminará los datos del dispositivo y de la aplicación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18677,6 +21047,12 @@ "value" : "Kunne ikke kode meddelelsens indhold" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo codificar el contenido del mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18711,6 +21087,12 @@ }, "Failed to exchange user info." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo intercambiar información de usuario." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -18727,6 +21109,12 @@ "value" : "Kunne ikke få en gyldig position til udveksling" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo obtener una posición válida para intercambiar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18767,6 +21155,12 @@ "value" : "Kunne ikke få en gyldig position til at bytte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo obtener una posición válida para intercambiar." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18814,6 +21208,12 @@ "value" : "Ordentliche Signalstärke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feria" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18860,6 +21260,12 @@ "value" : "Favorit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18901,6 +21307,12 @@ "value" : "Knoten, die als Favorit markiert oder ignoriert wurden, bleiben immer erhalten. Knoten ohne PKC-Schlüssel werden gemäß dem festgelegten Zeitplan aus der App-Datenbank gelöscht. Knoten mit PKC-Schlüsseln werden nur gelöscht, wenn das Intervall auf 7 Tage oder länger eingestellt ist. Diese Funktion löscht nur Knoten aus der App, die nicht in der Geräteknoten-Datenbank gespeichert sind." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los nodos favoritos e ignorados siempre se conservan. Los nodos sin claves PKC se borran de la base de datos de la aplicación según el cronograma establecido por el usuario, los nodos con claves PKC se borran solo si el intervalo se establece en 7 días o más. Esta función solo elimina los nodos de la aplicación que no están almacenados en la base de datos de nodos del dispositivo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18917,6 +21329,12 @@ }, "Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los nodos favoritos e ignorados siempre se conservan. Otros nodos se borran de la base de datos de la aplicación según el cronograma establecido por el usuario. (Los nodos con claves PKC siempre se conservan durante al menos 7 días). Esta función solo elimina los nodos de la aplicación que no están almacenados en la base de datos de nodos del dispositivo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -18939,6 +21357,12 @@ "value" : "Favoriten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18985,6 +21409,12 @@ "value" : "Favoriten und Knoten mit aktuellen Nachrichten werden oben in der Kontaktliste angezeigt." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los favoritos y los nodos con mensajes recientes aparecen en la parte superior de la lista de contactos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19037,6 +21467,12 @@ "value" : "Letzte Position eines Knotens holen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtener la última posición de un nodo determinado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19077,6 +21513,12 @@ "value" : "Femten minutter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "quince minutos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19124,6 +21566,12 @@ "value" : "Fünfzehn Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "quince segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19194,6 +21642,12 @@ "value" : "Filopbevaring" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenamiento de archivos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19229,6 +21683,12 @@ "Files Available" : { "comment" : "Data source label when files exist but none are active", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos disponibles" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -19251,6 +21711,12 @@ "value" : "Filtere die Knotenliste und die Mesh-Karte nach der Nähe zu deinem Handy." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtre la lista de nodos y el mapa de malla según la proximidad a su teléfono." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -19279,6 +21745,12 @@ "value" : "Kontakt suchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "encontrar un contacto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19325,6 +21797,12 @@ "value" : "Einen Knoten finden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encuentra un nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19371,6 +21849,12 @@ "value" : "Beenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "terminar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19442,6 +21926,12 @@ "value" : "Ziel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "terminar" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -19470,6 +21960,12 @@ "value" : "Firmware" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19516,6 +22012,12 @@ "value" : "Firmware opdateringsdokumenter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentos de actualización de firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19568,6 +22070,12 @@ "value" : "Firmwareaktualisierungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizaciones de firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19620,6 +22128,12 @@ "value" : "Firmware Version" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19690,6 +22204,12 @@ "value" : "Første gang hørt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "escuchado por primera vez" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19737,6 +22257,12 @@ "value" : "Fünf Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19813,6 +22339,12 @@ "value" : "Fünf Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco minutos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19860,6 +22392,12 @@ "value" : "Fünf Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19936,6 +22474,12 @@ "value" : "Feste PIN" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pasador fijo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20006,6 +22550,12 @@ "value" : "Fast position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición fija" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20046,6 +22596,12 @@ "value" : "Vend Skærm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltear pantalla" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20086,6 +22642,12 @@ "value" : "Vend skærm lodret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltear la pantalla verticalmente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20133,6 +22695,12 @@ "value" : "Folgen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguir" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20210,6 +22778,12 @@ "value" : "Folgen mit Steuerkurs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguir con encabezado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20280,6 +22854,12 @@ "value" : "For al MQTT-funktionalitet bortset fra kortrapporten skal du også indstille uplink og downlink for hver kanal, du vil forbinde til, over MQTT." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para todas las funciones de Mqtt además del informe de mapa, también debe configurar el enlace ascendente y descendente para cada canal que desee conectar a través de Mqtt." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20332,6 +22912,12 @@ "value" : "Für alle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para todos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20378,6 +22964,12 @@ "value" : "Für mich" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "para mi" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20425,6 +23017,12 @@ "value" : "Achtundvierzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuarenta y ocho horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20502,6 +23100,12 @@ "value" : "Fündundvierzig Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuarenta y cinco segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20579,6 +23183,12 @@ "value" : "Vier Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuatro horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20656,6 +23266,12 @@ "value" : "Vier Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuatro segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20732,6 +23348,12 @@ "value" : "Frequenz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20772,6 +23394,12 @@ "value" : "Frekvensoverride" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anulación de frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20812,6 +23440,12 @@ "value" : "Frekvensplads" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ranura de frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20852,6 +23486,12 @@ "value" : "Venligt navn" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre amigable" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20898,6 +23538,12 @@ "value" : "Venligt navn, der bruges til at formatere beskeder sendt til mesh. Eksempel: Et navn \"Motion\" ville resultere i en besked \"Motion detected\"" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre descriptivo utilizado para formatear el mensaje enviado a la malla. Ejemplo: un nombre \"Movimiento\" daría como resultado un mensaje \"Movimiento detectado\"." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20938,6 +23584,12 @@ }, "From Radio (RX): %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De Radio (RX): %lld" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -20960,6 +23612,12 @@ "value" : "Fuld support" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soporte completo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20995,6 +23653,12 @@ "Generate a data package (.zip) to configure TAK clients to connect to this server." : {}, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genere una nueva clave privada para reemplazar la que está actualmente en uso. La clave pública se regenerará automáticamente a partir de su clave privada." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21029,6 +23693,12 @@ "value" : "QR Code Erzeugen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Generar código QR" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21099,6 +23769,12 @@ "value" : "Få brugerdefinerede vandtætte sol- og detektionssensorroutere, aluminium desktop-noder og robuste håndsæt." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga nodos de enrutador de sensores de detección y solares impermeables personalizados, nodos de escritorio de aluminio y teléfonos resistentes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21145,6 +23821,12 @@ "value" : "Knotenposition ermitteln" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtener la posición del nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21191,6 +23873,12 @@ "value" : "Hent NRF DFU fra App Store" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga NRF DFU en la App Store" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21237,6 +23925,12 @@ "value" : "Los geht's" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "empezar" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -21259,6 +23953,12 @@ "value" : "Hent den nyeste stabile firmware" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga el firmware estable más reciente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21299,6 +23999,12 @@ }, "GitHub Repository" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repositorio GitHub" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21328,6 +24034,12 @@ "value" : "Godt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bueno" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21368,6 +24080,12 @@ "value" : "GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21414,6 +24132,12 @@ "value" : "GPIO-outputvarighed" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duración de la salida GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21454,6 +24178,12 @@ "value" : "GPIO-pin for drejeenkoder A-port" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para el puerto A del codificador rotatorio." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21494,6 +24224,12 @@ "value" : "GPIO-pin til drejeenkoder B-port." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para el puerto B del codificador rotatorio." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21534,6 +24270,12 @@ "value" : "GPIO-pin til roterende enkoder Press-port" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para codificador rotatorio Puerto de prensa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21574,6 +24316,12 @@ "value" : "GPIO-pin til overvågning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para monitorear" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21614,6 +24362,12 @@ "value" : "GPS PÅ GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS Y GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21660,6 +24414,12 @@ "value" : "GPS Indgang GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recepción GPS GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21706,6 +24466,12 @@ "value" : "GPS Send GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión GPS GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21758,6 +24524,12 @@ "value" : "Gruppennachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje grupal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21804,6 +24576,12 @@ "value" : "Stød %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ráfagas %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21845,6 +24623,12 @@ "value" : "HaHa" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jaja" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -21891,6 +24675,12 @@ }, "Hard Reset" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecimiento completo" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -21913,6 +24703,12 @@ "value" : "Hardware" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ferretería" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21960,6 +24756,12 @@ "value" : "Farlig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peligroso" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22000,6 +24802,12 @@ "value" : "Retning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "rumbo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22046,6 +24854,12 @@ "value" : "Kurs: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Título: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22093,6 +24907,12 @@ "value" : "Gehört" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "escuchado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22170,6 +24990,12 @@ "value" : "Herz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "corazón" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22240,6 +25066,12 @@ "value" : "Skjul alarmer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22280,6 +25112,12 @@ "value" : "Skjul Alarmer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22326,6 +25164,12 @@ "value" : "HOCH" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ALTA" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22379,6 +25223,12 @@ "value" : "Wandern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senderismo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22425,6 +25275,12 @@ "value" : "Historik Return Max" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historial Retorno Max" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22465,6 +25321,12 @@ "value" : "Vindue for historikreturnering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ventana de retorno del historial" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22511,6 +25373,12 @@ "value" : "Hops Entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "salta lejos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22557,6 +25425,12 @@ "value" : "Hops Entfernt %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salta lejos %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22604,6 +25478,12 @@ "value" : "Hops Entfernt:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saltos lejos:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22650,6 +25530,12 @@ "value" : "Hops Entfernt: %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salta lejos: %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22696,6 +25582,12 @@ "value" : "Stunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22742,6 +25634,12 @@ "value" : "Driftcyklus pr. time" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclo de trabajo por hora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22782,6 +25680,12 @@ "value" : "Hvor lang tid skærmen forbliver tændt, efter brugeren har trykket på knappen, eller meddelelser er modtaget." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuánto tiempo permanece encendida la pantalla después de presionar el botón de usuario o recibir mensajes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22828,6 +25732,12 @@ "value" : "Hvor ofte enhedens metrik sendes ud over mesh-netværket. Standard er 30 minutter." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían las métricas del dispositivo a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22874,6 +25784,12 @@ "value" : "Hvor ofte miljømålinger sendes ud over netværket. Standard er 30 minutter." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían métricas ambientales a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22920,6 +25836,12 @@ "value" : "Hvor ofte effektmålinger sendes ud over mesh-netværket. Standardindstillingen er 30 minutter." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían métricas de potencia a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22966,6 +25888,12 @@ "value" : "Hvor ofte skal vi forsøge at få en GPS-position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Con qué frecuencia debemos intentar obtener una posición GPS?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23012,6 +25940,12 @@ "value" : "Hvor ofte tilstanden for detektionssensoren skal sendes til mesh uanset detektion. Standard er Aldrig." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia enviar el estado del sensor de detección a la malla independientemente de la detección. El valor predeterminado es Nunca." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23064,6 +25998,12 @@ "value" : "How often we can send a message to the mesh when people are detected." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia podemos enviar un mensaje a la malla cuando se detectan personas." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -23134,6 +26074,12 @@ "value" : "Wie wird die Firmware aktualisiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cómo actualizar el firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23181,6 +26127,12 @@ "value" : "Brum" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tararear" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23227,6 +26179,12 @@ "value" : "Luftfeuchtigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Humedad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23268,6 +26226,12 @@ "value" : "Hybrid" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Híbrido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23339,6 +26303,12 @@ "value" : "Hybrid Luftfoto" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paso elevado híbrido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23409,6 +26379,12 @@ "value" : "Jeg har læst og forstået ovenstående. Jeg giver frivilligt samtykke til ukrypteret transmission af mine node-data via MQTT." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "He leído y entiendo lo anterior. Doy mi consentimiento voluntariamente para la transmisión sin cifrar de los datos de mi nodo a través de MQTT." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -23443,6 +26419,12 @@ "value" : "IAQ" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23489,6 +26471,12 @@ "value" : "IAQ " } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ " + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23535,6 +26523,12 @@ "value" : "IAQ %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23587,6 +26581,12 @@ "value" : "Emoji" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icono" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23627,6 +26627,12 @@ }, "Icons" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iconos" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -23649,6 +26655,12 @@ "value" : "Hvis DOP er indstillet, brug HDOP / VDOP værdier i stedet for PDOP" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si se configura DOP, use valores HDOP/VDOP en lugar de PDOP" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23695,6 +26707,12 @@ "value" : "Hvis aktiveret, vil 'output'-pinden blive trukket aktiv høj, deaktiveret betyder aktiv lav" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si está habilitado, el pin de 'salida' se activará alto, deshabilitado significa activo bajo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23741,6 +26759,12 @@ "value" : "Hvis det er svært at få adgang til din enheds nulstillingsknap, skal du gå ind i DFU-tilstand her." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si le resulta difícil acceder al botón de reinicio de su dispositivo, ingrese al modo DFU aquí." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23787,6 +26811,12 @@ "value" : "Hvis indstillet, vil alle pakker, du sender, blive sendt tilbage til din enhed." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si está configurado, cualquier paquete que envíe se enviará a su dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23833,6 +26863,12 @@ "value" : "Hvis standardregionsemnet er for travlt, kan du vælge et mere lokalt emne." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si el tema de la región predeterminada está demasiado ocupado, puede elegir un tema más local." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23879,6 +26915,12 @@ "value" : "Ignorer MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignorar MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23925,6 +26967,12 @@ "value" : "Ignorer node" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignorar nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23971,6 +27019,12 @@ "value" : "Ignoreret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ignorado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24018,6 +27072,12 @@ "value" : "Ignorerer observerede meddelelser fra fremmede mesh-netværk ligesom kun lokale, men tager det et skridt videre ved også at ignorere meddelelser fra noder, der ikke allerede er på nodens kendte liste." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignora los mensajes observados de mallas externas como Solo local, pero va un paso más allá al ignorar también los mensajes de nodos que aún no están en la lista conocida del nodo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24053,6 +27113,12 @@ "value" : "Ignorerer observerede meddelelser fra fremmede netværk, der er åbne, eller dem, som den ikke kan dekryptere. Genudsender kun meddelelser på noderne lokale primære / sekundære kanaler." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignora los mensajes observados de mallas externas que están abiertas o aquellas que no puede descifrar. Sólo retransmite mensajes en los canales primarios/secundarios locales del nodo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24097,6 +27163,12 @@ "value" : "Route importieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de importación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24137,6 +27209,12 @@ }, "In addition to Config, Keys and BLE bonds will be wiped" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Además de la configuración, se borrarán las claves y los enlaces BLE." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -24165,6 +27243,12 @@ "value" : "Include" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "incluir" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24235,6 +27319,12 @@ "value" : "Eingehende Nachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes entrantes" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -24258,6 +27348,12 @@ "value" : "Unvollständig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Incompleto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24305,6 +27401,12 @@ "value" : "Indien" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "India" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24340,6 +27442,12 @@ "value" : "Indendørs luftkvalitet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calidad del aire interior" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24386,6 +27494,12 @@ "value" : "Indeklimakvalitet (IAQ)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calidad del aire interior (IAQ)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24439,6 +27553,12 @@ "value" : "Router - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Dieser Knoten wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo de infraestructura únicamente en una torre o cima de una montaña. No debe usarse para techos o nodos móviles. Necesita una cobertura excepcional. Visible en la lista de nodos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24509,6 +27629,12 @@ "value" : "Input" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entradas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24555,6 +27681,12 @@ "value" : "Ungültiger Dateiinhalt. Bitte überprüfe das Dateiformat." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contenido del archivo no válido. Por favor verifique el formato del archivo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -24578,6 +27710,12 @@ "value" : "Omvendt topbjælke til 2-farvevisning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barra superior invertida para pantalla de 2 colores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24625,6 +27763,12 @@ "value" : "Japan" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Japón" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24665,6 +27809,12 @@ "value" : "JSON aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24711,6 +27861,12 @@ "value" : "JSON-tilstand er en begrænset, ukrypteret MQTT-udgang til lokal integration med Home Assistant" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El modo JSON es una salida MQTT limitada y sin cifrar para la integración local con el asistente doméstico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24757,6 +27913,12 @@ "value" : "Gå til nutid" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saltar al presente" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24797,6 +27959,12 @@ "value" : "Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "clave" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24837,6 +28005,12 @@ }, "Key Backup" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia de seguridad clave" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24865,6 +28039,12 @@ "value" : "Tastetilknytning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapeo de claves" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24911,6 +28091,12 @@ "value" : "Schlüsselgröße" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño de clave" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24952,6 +28138,12 @@ "value" : "Korea" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24998,6 +28190,12 @@ "value" : "Zuletzt gehört" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escuchado por última vez" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25040,6 +28238,12 @@ "comment" : "A label displayed next to the last seen device text in the `DeviceConnectRow`.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo visto por última vez:" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -25050,6 +28254,12 @@ }, "Last seen device: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Último dispositivo visto: %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -25072,6 +28282,12 @@ "value" : "Breitengrad" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25112,6 +28328,12 @@ }, "Latitude in degrees (e.g., 37.7749)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latitud en grados (por ejemplo, 37,7749)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -25134,6 +28356,12 @@ }, "Latitude must be between -90 and 90 degrees" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La latitud debe estar entre -90 y 90 grados." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -25162,6 +28390,12 @@ "value" : "LED-hjertebanken" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latido del corazón LED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25214,6 +28448,12 @@ "value" : "LED Status" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado del LED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25267,6 +28507,12 @@ "value" : "Links" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izquierda" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25343,6 +28589,12 @@ "value" : "Level" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25413,6 +28665,12 @@ "value" : "Licenseret operatør" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Operador Licenciado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25459,6 +28717,12 @@ "value" : "Begræns alle periodiske udsendelsesintervaller, især telemetri og position. Hvis du har brug for at øge antallet af hop, skal du gøre det på noder i kanterne, ikke dem i midten. MQTT anbefales ikke, når du er begrænset af duty cycle, fordi gateway-noden så udfører alt arbejdet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite todos los intervalos de transmisión periódica, especialmente la telemetría y la posición. Si necesita aumentar los saltos, hágalo en los nodos de los bordes, no en los del medio. No se recomienda MQTT cuando el ciclo de trabajo está restringido porque el nodo de puerta de enlace está haciendo todo el trabajo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25505,6 +28769,12 @@ "value" : "Linjeserie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie de línea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25545,6 +28815,12 @@ "value" : "Indlæser logfiler. . ." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargando registros. . ." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25587,6 +28863,12 @@ "comment" : "A label displayed above the options for local network access.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acceso a la red local" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -25615,6 +28897,12 @@ "value" : "Standort:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25667,6 +28955,12 @@ "value" : "Gesperrt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bloqueado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25713,6 +29007,12 @@ "value" : "Logniveauer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niveles de registro" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25765,6 +29065,12 @@ "value" : "Logging" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25835,6 +29141,12 @@ "value" : "Logfiler" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25882,6 +29194,12 @@ "value" : "Logfiler:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25934,6 +29252,12 @@ "value" : "Langer Name" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre largo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25986,6 +29310,12 @@ "value" : "Durch langes Gedrückthalten kannst du den Kontakt zu deinen Favoriten hinzufügen, stumm schalten oder eine Unterhaltung löschen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantenga presionado para marcar como favorito, silenciar el contacto o eliminar una conversación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26033,6 +29363,12 @@ "value" : "Lang Rækkevidde - Hurtig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26074,6 +29410,12 @@ "value" : "Lang rækkevidde - Moderat" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Moderado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26115,6 +29457,12 @@ "value" : "Lang rækkevidde - Langsom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26161,6 +29509,12 @@ "value" : "Längengrad" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26201,6 +29555,12 @@ }, "Longitude in degrees (e.g., -122.4194)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud en grados (p. ej., -122,4194)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -26223,6 +29583,12 @@ }, "Longitude must be between -180 and 180 degrees" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La longitud debe estar entre -180 y 180 grados." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -26257,6 +29623,12 @@ "value" : "LoRa" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "lora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26333,6 +29705,12 @@ "value" : "LoRa Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración LoRa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26397,6 +29775,12 @@ }, "LoRa Config Changes:" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambios en la configuración de LoRa:" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -26426,6 +29810,12 @@ "value" : "LoRa config empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de LoRa recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26503,6 +29893,12 @@ "value" : "Tracker" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetos perdidos y encontrados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26549,6 +29945,12 @@ "value" : "LAV" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "BAJO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26589,6 +29991,12 @@ "value" : "Niedriger Akkustand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batería baja" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -26618,6 +30026,12 @@ "value" : "M5 Stack Card KB / RAK Tastenfeld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarjeta de pila M5 Teclado KB / RAK" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26671,6 +30085,12 @@ "value" : "Malaysia 433 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malasia 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26712,6 +30132,12 @@ "value" : "Malaysia 919MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malasia 919MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26758,6 +30184,12 @@ "value" : "Kanäle verwalten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar canales" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26823,6 +30255,12 @@ "Manage custom map overlays" : { "comment" : "Subtitle for map data management", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar superposiciones de mapas personalizados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26870,6 +30308,12 @@ "value" : "Kartendaten verwalten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar datos de mapas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -26892,6 +30336,12 @@ "value" : "Administreret enhed" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo administrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26932,6 +30382,12 @@ }, "Manual" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "manuales" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -26961,6 +30417,12 @@ "value" : "Manuelle Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración manual" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27025,6 +30487,12 @@ }, "Manual connection string" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadena de conexión manual" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27041,6 +30509,12 @@ }, "Manual Connections" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conexiones manuales" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27051,6 +30525,12 @@ }, "Map Data" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos del mapa" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27079,6 +30559,12 @@ "value" : "Kartenoptionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27125,6 +30611,12 @@ "value" : "Karten-Overlays" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Superposiciones de mapas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27196,6 +30688,12 @@ "value" : "Kortudgivelsesinterval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de publicación de mapas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27242,6 +30740,12 @@ "value" : "Kortrapport" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe de mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27295,6 +30799,12 @@ "value" : "Maximale Wiederholungen erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retransmisión máxima alcanzada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27366,6 +30876,12 @@ "value" : "Mellem rækkevidde - Hurtig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rango medio - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27407,6 +30923,12 @@ "value" : "Mellem rækkevidde - Langsom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rango medio - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27447,6 +30969,12 @@ "value" : "Opdatering af meshningsaktivitet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización de actividad de malla" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27493,6 +31021,12 @@ "value" : "Mesh Live Aktivität" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actividad en vivo de malla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27569,6 +31103,12 @@ "value" : "Mesh Karte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapa de malla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27639,6 +31179,12 @@ "value" : "Standort auf der Mesh-Karte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación del mapa de malla" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27655,6 +31201,12 @@ }, "Meshtastic" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtástico" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27671,6 +31223,12 @@ }, "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic no recopila ninguna información personal. Recopilamos de forma anónima datos de uso y fallos para mejorar la aplicación. Puede optar por no participar en la configuración de la aplicación." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27693,6 +31251,12 @@ "value" : "Meshtastic Knoten %@ hat Kanäle mit dir geteilt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Node %@ ha compartido canales contigo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27733,6 +31297,12 @@ "value" : "Meshtastic verwendet den Standort deines Handys, um eine Reihe von Funktionen zu ermöglichen. Du kannst deine Standortberechtigungen jederzeit in den Einstellungen aktualisieren." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic utiliza la ubicación de su teléfono para habilitar una serie de funciones. Puede actualizar sus permisos de ubicación en cualquier momento desde la configuración." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -27755,6 +31325,12 @@ "value" : "Meshtastic® er copyright Meshtastic LLC" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic® Copyright Meshtastic LLC" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27801,6 +31377,12 @@ "value" : "Nachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27847,6 +31429,12 @@ "value" : "Nachrichteninhalt überschreitet 200 Bytes." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El contenido del mensaje supera los 200 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27893,6 +31481,12 @@ "value" : "Nachrichtendetails" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27970,6 +31564,12 @@ "value" : "Nachricht von der Textnachricht-App empfangen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje recibido de la aplicación de mensajes de texto." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28035,6 +31635,12 @@ "Message Size" : { "comment" : "VoiceOver label for message size", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del mensaje" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28063,6 +31669,12 @@ "value" : "Beskedstatusindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de estado del mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28109,6 +31721,12 @@ "value" : "Nachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -28179,6 +31797,12 @@ "value" : "Nachrichten getrennt mit |" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se separan con |" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28213,6 +31837,12 @@ }, "Messaging" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajería" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28242,6 +31872,12 @@ "value" : "Metrisk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métrica" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28289,6 +31925,12 @@ "value" : "Mittag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "mediodía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28335,6 +31977,12 @@ "value" : "Minimum Distanz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distancia mínima" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28382,6 +32030,12 @@ "value" : "Minimum Intervall" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo mínimo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28423,6 +32077,12 @@ "value" : "Minimum tid mellem detektionsudsendelser" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo mínimo entre transmisiones de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28463,6 +32123,12 @@ "value" : "Minimaltid mellem detektion broadcasts. Standard er 45 sekunder." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo mínimo entre transmisiones de detección. El valor predeterminado es 45 segundos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28509,6 +32175,12 @@ "value" : "Modus" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28579,6 +32251,12 @@ "value" : "Model" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "modelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28626,6 +32304,12 @@ "value" : "Moderat" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "moderado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28678,6 +32362,12 @@ "value" : "Modul Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28755,6 +32445,12 @@ "value" : "Morgen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "mañana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28801,6 +32497,12 @@ "value" : "Die meisten Daten in deinem Mesh werden über den primären Kanal gesendet. Du kannst sekundäre Kanäle einrichten, um zusätzliche Nachrichtengruppen zu erstellen, die durch ihren eigenen Schlüssel gesichert sind. [Tipps zur Kanalkonfiguration](https://meshtastic.org/docs/configuration/radio/channels/)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La mayoría de los datos de su malla se envían a través del canal principal. Puede configurar canales secundarios para crear grupos de mensajería adicionales protegidos por su propia clave. [Consejos de configuración de canales](https://meshtastic.org/docs/configuration/tips/)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28871,6 +32573,12 @@ "value" : "MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28923,6 +32631,12 @@ "value" : "MQTT Client Proxy" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proxy de cliente MQTT" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28999,6 +32713,12 @@ "value" : "MQTT Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración MQTT" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29076,6 +32796,12 @@ "value" : "MQTT Modulkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo MQTT recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29153,6 +32879,12 @@ "value" : "Multiplier" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplicador" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -29211,6 +32943,12 @@ "value" : "Skal være en enkelt emoji" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debe ser un solo emoji" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29258,6 +32996,12 @@ "value" : "MyInfo empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mi información recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29329,6 +33073,12 @@ "value" : "Banke-timeout" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo de espera de molestia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29369,6 +33119,12 @@ "value" : "Name" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29421,6 +33177,12 @@ "value" : "Name muss kürzer als 30 Bytes sein" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre debe tener menos de 30 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29467,6 +33229,12 @@ "value" : "Rutevejvisning til node" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navegar al nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29507,6 +33275,12 @@ "value" : "Nærliggende emner" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temas cercanos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29553,6 +33327,12 @@ "value" : "Netzwerk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29629,6 +33409,12 @@ "value" : "Netzwerkeinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de red" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29706,6 +33492,12 @@ "value" : "Netzwerkkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de red recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29776,6 +33568,12 @@ "value" : "Netværksstatus Orange" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado de la red naranja" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29822,6 +33620,12 @@ "value" : "Netværksstatus rød" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado de la red Rojo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29869,6 +33673,12 @@ "value" : "Ny node" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuevo nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29910,6 +33720,12 @@ "value" : "Ny node fundet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se ha descubierto un nuevo nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29950,6 +33766,12 @@ "value" : "Neue Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuevos nodos" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29973,6 +33795,12 @@ "value" : "New Zealand 865MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nueva Zelanda 865MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30019,6 +33847,12 @@ "value" : "Neuere Firmware ist verfügbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hay un firmware más nuevo disponible" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30072,6 +33906,12 @@ "value" : "Nacht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noche" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30119,6 +33959,12 @@ "value" : "NMEA Positionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones NMEA" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30196,6 +34042,12 @@ "value" : "Kein Kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30272,6 +34124,12 @@ "value" : "Kein verbundener Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ningún nodo conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30313,6 +34171,12 @@ "value" : "Keine Daten vorhanden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30365,6 +34229,12 @@ "value" : "Kein Gerät verbunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ningún dispositivo conectado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30435,6 +34305,12 @@ "value" : "Ingen enhedsdata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas de dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30475,6 +34351,12 @@ "value" : "Ingen miljødata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30521,6 +34403,12 @@ "value" : "Keine Dateien hochgeladen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se subieron archivos" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -30550,6 +34438,12 @@ "value" : "Keine Schnittstelle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin interfaz" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30615,6 +34509,12 @@ "No map data files uploaded" : { "comment" : "Message when no files are uploaded", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se han subido archivos de datos de mapas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -30637,6 +34537,12 @@ "value" : "Ingen PAX-logfiler" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin registros de contador de PAX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30690,6 +34596,12 @@ "value" : "Keine PIN (geht einfach)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin PIN (simplemente funciona)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30766,6 +34678,12 @@ "value" : "Keine Positionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin posiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30806,6 +34724,12 @@ "value" : "Ingen energidata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30853,6 +34777,12 @@ "value" : "Keine Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin respuesta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30930,6 +34860,12 @@ "value" : "Keine Route" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin ruta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31006,6 +34942,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31052,6 +34994,12 @@ "value" : "Node Core Data Backup %1$@/%2$@ - %3$@ - %4$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia de seguridad de datos del núcleo del nodo %@/%@ - %@ - %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31105,6 +35053,12 @@ "value" : "Knoten hat keine Position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nodo no tiene posiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31151,6 +35105,12 @@ "value" : "Knoten Historie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historia del nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31192,6 +35152,12 @@ "value" : "Node Info Broadcast Interval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión de información de nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31238,6 +35204,12 @@ "value" : "Knotenkarte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapa de nodos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31284,6 +35256,12 @@ "value" : "Knotennummer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31330,6 +35308,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31401,6 +35385,12 @@ "value" : "Knoten (%@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos (%@)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31478,6 +35468,12 @@ "value" : "Keins" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ninguno" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31548,6 +35544,12 @@ "value" : "Ikke en gyldig rute-fil" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No es un archivo de ruta válido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31595,6 +35597,12 @@ "value" : "Nicht authorisiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No autorizado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31666,6 +35674,12 @@ "value" : "Ikke til stede" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No presente" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31736,6 +35750,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31776,6 +35796,12 @@ "value" : "Mitteilungen für Kanal- und Direktnachrichten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones por canal y mensajes directos." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -31798,6 +35824,12 @@ "value" : "Mitteilungen bei niedrigem Akkustand des verbundenen Funkgeräts." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones de alertas de batería baja para el dispositivo conectado." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -31820,6 +35852,12 @@ "value" : "Mitteilungen für neu entdeckte Knoten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones para nodos recién descubiertos." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -31848,6 +35886,12 @@ "value" : "Anzahl Hops" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de saltos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31894,6 +35938,12 @@ "value" : "Anzahl Einträge" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de registros" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31940,6 +35990,12 @@ "value" : "Anzahl Satelliten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de satélites" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31974,6 +36030,12 @@ }, "Ok" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -32002,6 +36064,12 @@ "value" : "Ok" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32042,6 +36110,12 @@ "value" : "OK til MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok para MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32094,6 +36168,12 @@ "value" : "OLED Typ" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo OLED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32147,6 +36227,12 @@ "value" : "Nur beim Starten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sólo en el arranque" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32217,6 +36303,12 @@ "value" : "Onboarding af licenserede operatører kræver firmware 2.0.20 eller nyere. Sørg for at henvise til dine lokale regler og kontakt de lokale amatørfrekvenskoordinatorer med spørgsmål." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La incorporación de operadores con licencia requiere firmware 2.0.20 o superior. Asegúrese de consultar las regulaciones locales y comuníquese con los coordinadores locales de frecuencias de aficionados si tiene preguntas." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32269,6 +36361,12 @@ "value" : "Eine Stunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "una hora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32345,6 +36443,12 @@ "value" : "Eine Minute" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "un minuto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32422,6 +36526,12 @@ "value" : "Eine Sekunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "un segundo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32498,6 +36608,12 @@ "value" : "Online" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En línea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32539,6 +36655,12 @@ "value" : "Kun tilladt for SENSOR-, TRACKER- og TAK_TRACKER-roller, dette vil hæmme alle genudsendelser, ikke ulig CLIENT_MUTE-rollen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solo permitido para los roles SENSOR, TRACKER y TAK_TRACKER, esto inhibirá todas las retransmisiones, al igual que el rol CLIENT_MUTE." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -32574,6 +36696,12 @@ "value" : "Kun videresender pakker fra kerneportnumre: NodeInfo, Text, Position, Telemetry og Routing." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solo retransmite paquetes desde los portnums principales: NodeInfo, Texto, Posición, Telemetría y Enrutamiento." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -32602,6 +36730,12 @@ }, "Open Compass" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir brújula" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -32624,6 +36758,12 @@ "value" : "Einstellungen öffnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32671,6 +36811,12 @@ "value" : "Optimeret til 2-farve skærme" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optimizado para pantallas de 2 colores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32718,6 +36864,12 @@ "value" : "Optimiert für ATAK-Systemkommunikation, verringert die Anzahl der Routineübertragungen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optimizado para la comunicación del sistema ATAK, reduce las transmisiones de rutina." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32788,6 +36940,12 @@ "value" : "Valgfrie felter at inkludere, når positionsmeddelelser samles. Jo flere felter, der inkluderes, jo større bliver meddelelsen - hvilket fører til længere sendetid og en højere risiko for pakkeloss" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Campos opcionales para incluir al ensamblar mensajes de posición. Cuantos más campos se incluyan, más grande será el mensaje, lo que llevará a un mayor tiempo de emisión y a un mayor riesgo de pérdida de paquetes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32834,6 +36992,12 @@ "value" : "Valgfri GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO opcional" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32880,6 +37044,12 @@ "value" : "Optionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32950,6 +37120,12 @@ "value" : "OS-logindlægdetaljer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles de entrada de registro del sistema operativo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32990,6 +37166,12 @@ "value" : "OTA-opdateringer understøttes ikke på denne NRF-enhed." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las actualizaciones OTA no son compatibles con este dispositivo NRF." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33036,6 +37218,12 @@ "value" : "OTA-opdateringer understøttes ikke på din platform." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las actualizaciones OTA no son compatibles con su plataforma." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33082,6 +37270,12 @@ "value" : "Andre datakilder" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otras fuentes de datos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33128,6 +37322,12 @@ "value" : "Ausgabe von Echtzeit-Fehlersuchprotokollen über die serielle Schnittstelle, Anzeige und Export von positionskorrigierten Geräteprotokollen über Bluetooth." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genere registros de depuración en vivo a través de serie, vea y exporte registros de dispositivos redactados en posición a través de Bluetooth." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33168,6 +37368,12 @@ "value" : "Output pin buzzer GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zumbador de pin de salida GPIO " + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33208,6 +37414,12 @@ "value" : "Output pin GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de salida GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33248,6 +37460,12 @@ "value" : "Output pin vibra GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de salida vibración GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33289,6 +37507,12 @@ "value" : "Overlanding" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por tierra" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33335,6 +37559,12 @@ "value" : "Tilsidesæt automatisk OLED-skærmdetektion." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anule la detección automática de pantalla OLED." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33369,6 +37599,12 @@ }, "Override default screen layout." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anular el diseño de pantalla predeterminado." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -33385,6 +37621,12 @@ }, "Packet Count" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuento de paquetes" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -33413,6 +37655,12 @@ "value" : "Pairing Modus" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de emparejamiento" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33489,6 +37737,12 @@ "value" : "Passwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseña" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33565,6 +37819,12 @@ "value" : "Pause" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pausa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33635,6 +37895,12 @@ "value" : "PAX tæller" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contador de pasajeros" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -33699,6 +37965,12 @@ "value" : "PAX-tæller konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del contador PAX" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -33758,6 +38030,12 @@ "value" : "PAX-tæller konfiguration modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del contador PAX recibida: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33804,6 +38082,12 @@ "value" : "PAX-tællerlog" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de contador de PAX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33857,6 +38141,12 @@ "value" : "PAX Counter message received for: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje del contador de PAX recibido de: %@" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -33921,6 +38211,12 @@ "value" : "paxcounter.log" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33961,6 +38257,12 @@ "value" : "Verbundenen Knoten auf Werkseinstellungen zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Realice un restablecimiento de fábrica en el nodo al que está conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34002,6 +38304,12 @@ "value" : "Filippinerne 433 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34043,6 +38351,12 @@ "value" : "Filippinerne 868 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34084,6 +38398,12 @@ "value" : "Filippinerne 915MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 915MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34130,6 +38450,12 @@ "value" : "Telefon GPS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS del teléfono" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34200,6 +38526,12 @@ "value" : "Standorteinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación del teléfono" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -34222,6 +38554,12 @@ "value" : "Fastgør %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34262,6 +38600,12 @@ "value" : "Fastgør A" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin A" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34302,6 +38646,12 @@ "value" : "Fastgør B" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin B" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34348,6 +38698,12 @@ "value" : "PKI-basierte Knotenadministration, benötigt Firmware Version 2.5+" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración de nodos basada en PKI, requiere versión de firmware 2.5+" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34394,6 +38750,12 @@ "value" : "Vær opmærksom på, at fordi kortrapporten ikke er krypteret, kan dine data blive gemt og vist permanent af tredjeparter. Meshtastic påtager sig ikke ansvar for lagring, visning eller offentliggørelse af disse data." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tenga en cuenta que debido a que el informe del mapa no está cifrado, terceros pueden almacenar y mostrar sus datos de forma permanente. Meshtastic no asume responsabilidad por dicho almacenamiento, exhibición o divulgación de estos datos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -34434,6 +38796,12 @@ "value" : "Bitte verbinde dich mit einem Funkgerät, um die Einstellungen zu ändern." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conéctese a una radio para configurar los ajustes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34487,6 +38855,12 @@ "value" : "Bitte lege eine Region fest" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor establece una región" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34527,6 +38901,12 @@ "value" : "Interessante steder" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puntos de interés" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34574,6 +38954,12 @@ "value" : "Kacke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "caca" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34645,6 +39031,12 @@ "value" : "Placering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -34709,6 +39101,12 @@ "value" : "Positionseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de posición" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34780,6 +39178,12 @@ "value" : "Positionskonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de posición recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34844,6 +39248,12 @@ "value" : "Placering udveksling mislykkedes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error en el intercambio de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34884,6 +39294,12 @@ "value" : "Positionsudveksling anmodet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intercambio de posición solicitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34924,6 +39340,12 @@ "value" : "Positionsflag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Banderas de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34964,6 +39386,12 @@ "value" : "Positionslog" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35004,6 +39432,12 @@ "value" : "Positionslog %lld punkter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de posición %lld Puntos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35044,6 +39478,12 @@ "value" : "Position pakke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paquete de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35090,6 +39530,12 @@ "value" : "Position gesendet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35130,6 +39576,12 @@ "value" : "Positioner aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones Habilitadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35176,6 +39628,12 @@ "value" : "Positioner vil blive angivet af din enheds GPS, hvis du vælger deaktiveret eller ikke til stede, kan du indstille en fast position." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las posiciones serán proporcionadas por el GPS de su dispositivo; si selecciona deshabilitado o no presente, puede establecer una posición fija." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35228,6 +39686,12 @@ "value" : "Strom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "poder" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -35298,6 +39762,12 @@ "value" : "Stromkonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -35363,6 +39833,12 @@ "value" : "Strømkonfiguration modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de energía recibida: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35410,6 +39886,12 @@ "value" : "Energidata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35450,6 +39932,12 @@ "value" : "Energidata-log" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35490,6 +39978,12 @@ "value" : "Sluk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apagar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35531,6 +40025,12 @@ "value" : "Strømindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35571,6 +40071,12 @@ "value" : "Stromsparen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahorro de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -35635,6 +40141,12 @@ "value" : "Strøm Skærm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35669,6 +40181,12 @@ }, "Power Sensor Options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores de potencia" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -35691,6 +40209,12 @@ "value" : "Angeschaltet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrollado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35737,6 +40261,12 @@ "value" : "Genaue Position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación precisa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35783,6 +40313,12 @@ "value" : "Voreinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preajustes" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35829,6 +40365,12 @@ "value" : "Tryk fastgør" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de prensa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35869,6 +40411,12 @@ "value" : "Tryk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35915,6 +40463,12 @@ "value" : "Primär" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primaria" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35991,6 +40545,12 @@ "value" : "Erster Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador principal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36037,6 +40597,12 @@ "value" : "Primær GPIO" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO primario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36083,6 +40649,12 @@ "value" : "Privater Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave privada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36136,6 +40708,12 @@ "value" : "Prozess" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proceso" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36206,6 +40784,12 @@ "value" : "Datei wird verarbeitet…" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Procesando archivo..." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36258,6 +40842,12 @@ "value" : "Projektinformationen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Información del proyecto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36311,6 +40901,12 @@ "value" : "Protobufs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protobufs" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36381,6 +40977,12 @@ "value" : "Teile anonyme Nutzungsstatistiken und Absturzberichte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proporcione estadísticas de uso anónimas e informes de fallos." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -36397,6 +40999,12 @@ }, "Provide Confirmation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proporcionar confirmación" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -36425,6 +41033,12 @@ "value" : "Öffentlicher Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave pública" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36471,6 +41085,12 @@ "value" : "Offentlig nøglekryptering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cifrado de clave pública" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36517,6 +41137,12 @@ "value" : "Offentlig nøgle uoverensstemmelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública no coincide" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36563,6 +41189,12 @@ "value" : "PWD" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PCD" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36610,6 +41242,12 @@ "value" : "Fragezeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pregunta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36680,6 +41318,12 @@ "value" : "Stråling" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radiación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36726,6 +41370,12 @@ "value" : "Geräteeinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de radio" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36803,6 +41453,12 @@ "value" : "RAK Drehimpulsgeber Modul" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codificador rotatorio RAK" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36879,6 +41535,12 @@ "value" : "Entfernungstest" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prueba de rango" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36955,6 +41617,12 @@ "value" : "Entfernungstest Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de prueba de rango" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37032,6 +41700,12 @@ "value" : "Range Test Modul konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de prueba de rango recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37108,6 +41782,12 @@ "value" : "Neustart" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37184,6 +41864,12 @@ "value" : "Knoten neustarten?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Reiniciar el nodo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37255,6 +41941,12 @@ "value" : "Genudsend enhver observeret besked, hvis den var på vores private kanal eller fra et andet netværk med de samme lora-parametre." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retransmitir cualquier mensaje observado, si fue en nuestro canal privado o desde otra malla con los mismos parámetros de lora." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -37289,6 +41981,12 @@ "value" : "Genudsendelsestilstand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de retransmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37335,6 +42033,12 @@ "value" : "Modtage data (rxd) GPIO-pin" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibir datos (rxd) pin GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37382,6 +42086,12 @@ "value" : "Negative Empfangsbestätigung empfangen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibí un reconocimiento negativo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37459,6 +42169,12 @@ "value" : "Empfangsbestätigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación recibida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37517,6 +42233,12 @@ }, "Received Ack: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación recibida: %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -37540,6 +42262,12 @@ "value" : "Recipient Ack" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación del destinatario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37598,6 +42326,12 @@ }, "Recipient Ack: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación del destinatario: %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -37620,6 +42354,12 @@ "value" : "Route aufzeichnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de grabación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37660,6 +42400,12 @@ "value" : "Opdater enhedsmetadata" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar metadatos del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37694,6 +42440,12 @@ }, "Regenerate Private Key" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Regenerar clave privada" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -37728,6 +42480,12 @@ "value" : "Region" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Región" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37781,6 +42539,12 @@ "value" : "Regionale Einschaltdauergrenze erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se alcanzó el límite del ciclo de trabajo regional" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37851,6 +42615,12 @@ "value" : "Relayed by %1$d %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retransmitido por %d %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -37867,6 +42637,12 @@ "value" : "Udgivelsesnoter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas de la versión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37908,6 +42684,12 @@ "value" : "Fjernadministration for: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración remota para: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37948,6 +42730,12 @@ "value" : "Fjern Legacy Admin: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrador remoto heredado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37988,6 +42776,12 @@ "value" : "Fjern-PKI-Admin: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrador remoto de PKI: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38034,6 +42828,12 @@ "value" : "Entfernen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38080,6 +42880,12 @@ "value" : "Von Favoriten entfernen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar de favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38120,6 +42926,12 @@ "value" : "Fjern fra ignoreret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar de ignorado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38173,6 +42985,12 @@ "value" : "Repeater" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "repetidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38219,6 +43037,12 @@ "value" : "Erstat kanaler" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reemplazar canales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38265,6 +43089,12 @@ "value" : "Antworten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Responder" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38335,6 +43165,12 @@ "value" : "Anmod om administrator (gammel): %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitar administrador heredado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38375,6 +43211,12 @@ "value" : "Anmod om PKI Admin: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitar administrador de PKI: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38416,6 +43258,12 @@ "value" : "Anmodet modulmeddelelser for færdiglavede meddelelser til node: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes del módulo de mensajes predefinidos solicitados para el nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38486,6 +43334,12 @@ "value" : "Kræver, at der er et accelerometer på din enhed." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requiere que haya un acelerómetro en su dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38532,6 +43386,12 @@ "value" : "App-Einstellungen zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecer la configuración de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38584,6 +43444,12 @@ "value" : "Knotendatenbank zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecer NodeDB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38631,6 +43497,12 @@ "value" : "Neustarten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38678,6 +43550,12 @@ "value" : "Verbundenen Knoten neustarten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinicie en el nodo al que está conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38712,6 +43590,12 @@ }, "Restore" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restaurar" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -38746,6 +43630,12 @@ "value" : "Fortsetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currículum" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38810,6 +43700,12 @@ }, "Retreiving nodes . ." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos. ." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -38827,6 +43723,12 @@ "Retreiving nodes %lld" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos %lld" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -38843,6 +43745,12 @@ }, "Retrieving nodes" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -38859,6 +43767,12 @@ }, "Retrieving nodes %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos %lld" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -38875,6 +43789,12 @@ }, "Retrying (attempt %lld)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentando (intento %lld)" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -38903,6 +43823,12 @@ "value" : "App bewerten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Revisa la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38956,6 +43882,12 @@ "value" : "Rechts" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Derecha" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39032,6 +43964,12 @@ "value" : "Klingelton" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tono de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39108,6 +44046,12 @@ "value" : "Klingelton Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de tono de llamada" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -39172,6 +44116,12 @@ "value" : "Sprog til overførsel af ringetoner" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idioma de transferencia de tono de llamada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39224,6 +44174,12 @@ "value" : "Ringtone Transfer Language (RTTTL) Ringtone String brugt af understøttede buzzere i eksterne meddelelser" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lenguaje de transferencia de tono de llamada (RTTTL) Cadena de tono utilizada por los timbres compatibles en notificaciones externas." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -39294,6 +44250,12 @@ "value" : "Rolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rol" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39341,6 +44303,12 @@ "value" : "Rolle: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rol: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39387,6 +44355,12 @@ "value" : "Rollen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Roles" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39427,6 +44401,12 @@ "value" : "Hovedemne" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema raíz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39467,6 +44447,12 @@ "value" : "Rotary 1" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giratorio 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39513,6 +44499,12 @@ "value" : "Returrute: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de regreso: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39553,6 +44545,12 @@ "value" : "Ruteliner" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Líneas de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39593,6 +44591,12 @@ "value" : "Routenliste" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista de rutas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -39621,6 +44625,12 @@ "value" : "Route aufzeichnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grabador de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39667,6 +44677,12 @@ "value" : "Routenaufzeichnung pausiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grabación de ruta en pausa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39713,6 +44729,12 @@ "value" : "Route: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39760,6 +44782,12 @@ "value" : "Router" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39813,6 +44841,12 @@ "value" : "Router mit Verzögerung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutador tarde" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39859,6 +44893,12 @@ "value" : "Routenliste" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rutas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39906,6 +44946,12 @@ "value" : "Routing empfangen für RequestID: %@ Ack Status: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutamiento recibido para ID de solicitud: %@ Estado de confirmación: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39976,6 +45022,12 @@ "value" : "RSSI %@ dBm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %@ dBm" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40016,6 +45068,12 @@ "value" : "RSSI %ddB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %ddB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40057,6 +45115,12 @@ "value" : "RSSI %llddB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %llddB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40104,6 +45168,12 @@ "value" : "RTTTL Klingeltonkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de tono de llamada RTTTL recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40175,6 +45245,12 @@ "value" : "Rusland" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rusia" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40209,6 +45285,12 @@ "value" : "Forstærket RX-forstærkning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ganancia impulsada por RX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40250,6 +45332,12 @@ "value" : "Samme som adfærd som ALL, men springer pakkedekodning over og genudsender dem blot. Kun tilgængelig i Repeater-rollen. At indstille dette på andre roller vil resultere i ALL-adfærd." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Igual que el comportamiento de ALL, pero omite la decodificación de paquetes y simplemente los retransmite. Sólo disponible en rol de Repetidor. Establecer esto en cualquier otro rol dará como resultado TODOS los comportamientos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40291,6 +45379,12 @@ "value" : "Satellit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satélite" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -40350,6 +45444,12 @@ "value" : "Satellitoverflyvning" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sobrevuelo satelital" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40426,6 +45526,12 @@ "value" : "Satelliten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "sábados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40472,6 +45578,12 @@ "value" : "Satelliten Schätzung %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estimación de satélites %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40518,6 +45630,12 @@ "value" : "Satelliten in Sicht: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sats a la vista: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40564,6 +45682,12 @@ "value" : "Speichern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40634,6 +45758,12 @@ "value" : "Gem kanalindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar configuración del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40681,6 +45811,12 @@ "value" : "Speichere Konfiguration für %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar configuración para %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40757,6 +45893,12 @@ "value" : "Benutzerkonfiguration nach %@ speichern?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Guardar configuración de usuario en %@?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40803,6 +45945,12 @@ "value" : "Gemmer en CSV-fil med detaljer om intervaltestbeskeder, i øjeblikket kun tilgængelig på ESP32-enheder med en webserver" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guarda un CSV con los detalles del mensaje de prueba de rango, actualmente solo disponible en dispositivos ESP32 con un servidor web." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40843,6 +45991,12 @@ }, "Scan this QR code to add %@ to another device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escanee este código QR para agregar %@ a otro dispositivo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40877,6 +46031,12 @@ "value" : "Skærm tændt i " } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla encendida para" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40923,6 +46083,12 @@ "value" : "Suchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buscar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40963,6 +46129,12 @@ "value" : "Sekund" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "segundo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41009,6 +46181,12 @@ "value" : "Sekundär" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secundaria" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41085,6 +46263,12 @@ "value" : "Zweiter Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador secundario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41138,6 +46322,12 @@ "value" : "Sicherheit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41190,6 +46380,12 @@ "value" : "Sicherheitskonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de seguridad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41242,6 +46438,12 @@ "value" : "Sicherheitskonfigurationseinstellungen erfordern eine Firmware mit Version 2.5 oder höher" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ajustes de configuración de seguridad requieren una versión de firmware 2.5+" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41295,6 +46497,12 @@ "value" : "Auswählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41371,6 +46579,12 @@ "value" : "Kanal wählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41411,6 +46625,12 @@ "value" : "Vælg en samtale" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione una conversación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41451,6 +46671,12 @@ "value" : "Vælg en samtaletype" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un tipo de conversación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41485,6 +46711,12 @@ }, "Select a Node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un nodo" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41501,6 +46733,12 @@ "value" : "Vælg en node fra listen for at (fjern)administrere enheden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un nodo del menú desplegable para administrar dispositivos conectados o remotos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -41535,6 +46773,12 @@ "value" : "Vælg en rutesporing (Trace route)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione una ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41569,6 +46813,12 @@ }, "Select an emoji" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecciona un emoji" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41585,6 +46835,12 @@ "value" : "Vælg kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41625,6 +46881,12 @@ "value" : "Datei auswählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar archivo de mapa" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41647,6 +46909,12 @@ "value" : "Als kritisch eingestufte Mitteilungen ignorieren den Stummschalter und die 'Nicht stören'-Einstellungen des Benachrichtigungszentrums." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los paquetes seleccionados enviados como críticos ignorarán el interruptor de silencio y la configuración de No molestar en el centro de notificaciones del sistema operativo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41675,6 +46943,12 @@ "value" : "Senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "enviar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41721,6 +46995,12 @@ "value" : "Sende ${messageContent} an ${channelNumber}" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar ${messageContent} a ${channelNumber}" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41761,6 +47041,12 @@ "value" : "Send Send ${messageContent} til ${nodeNumber}" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar ${messageContent} a ${nodeNumber}" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41801,6 +47087,12 @@ "value" : "Send en direkte besked" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje directo" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41829,6 +47121,12 @@ "value" : "Gruppennachricht senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje grupal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41869,6 +47167,12 @@ "value" : "Send et hjerteslag for at annoncere serverens tilstedeværelse." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envíe un latido para anunciar la presencia del servidor." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41909,6 +47213,12 @@ "value" : "Send en besked til én Meshtastic-kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje a un determinado canal meshtastic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41949,6 +47259,12 @@ "value" : "Send en besked til én Meshtastic-node" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje a un determinado nodo meshtastic" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -41971,6 +47287,12 @@ "value" : "Send en position på den primære kanal, når brugerknappen trykkes tre gange." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envíe una posición en el canal principal cuando se haga triple clic en el botón del usuario." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42023,6 +47345,12 @@ "value" : "Herunterfahren an verbundenen Knoten senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía un apagado al nodo al que estás conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42069,6 +47397,12 @@ "value" : "Wegpunkt senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42109,6 +47443,12 @@ "value" : "Send ASCII-klokke med advarselsbesked. Nyttig til at udløse ekstern notifikation ved bip." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar campana ASCII con mensaje de alerta. Útil para activar notificaciones externas al tocar el timbre." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42161,6 +47501,12 @@ "value" : "Sende Glocke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42213,6 +47559,12 @@ "value" : "Herzschlag senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar latido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42283,6 +47635,12 @@ "value" : "Mitteilungen senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar notificaciones" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -42305,6 +47663,12 @@ "value" : "Send genstart OTA" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar Reiniciar OTA" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42352,6 +47716,12 @@ "value" : "Afsenderinterval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo del remitente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42405,6 +47775,12 @@ "value" : "Sensor" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "sensores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42451,6 +47827,12 @@ "value" : "Sensorindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42492,6 +47874,12 @@ "value" : "Sensorindstillinger" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42527,6 +47915,12 @@ "value" : "Sendte en kanal for: %@ Kanal indeks %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviado un canal para: %@ Índice de canales %d" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42604,6 +47998,12 @@ "value" : "LoRa.Config gesendet für: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió un LoRa.Config para: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42681,6 +48081,12 @@ "value" : "Position von Apple Gerät an Knoten gesendet: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió un paquete de posición desde el dispositivo GPS de Apple al nodo: %@@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42758,6 +48164,12 @@ "value" : "Sende Traceroute Anforderung zu Knoten: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió una solicitud de ruta de seguimiento al nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42835,6 +48247,12 @@ "value" : "Wegpunkt gesendet von: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviado un paquete de waypoint desde: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42912,6 +48330,12 @@ "value" : "Sende Nachricht %@ von %@ an %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje enviado %@ de %@ a %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42988,6 +48412,12 @@ "value" : "Sequenznummer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de secuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43034,6 +48464,12 @@ "value" : "Sequenz: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secuencia: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43074,6 +48510,12 @@ "value" : "Seriel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43150,6 +48592,12 @@ "value" : "Serial Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración en serie" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43226,6 +48674,12 @@ "value" : "Serielle Konsole" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consola serie" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43272,6 +48726,12 @@ "value" : "Serielle Konsole über die Stream-API." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consola serial a través de Stream API." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43319,6 +48779,12 @@ "value" : "Serial Modul Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo serie recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43389,6 +48855,12 @@ "value" : "Serier" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43435,6 +48907,12 @@ "value" : "Server" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43481,6 +48959,12 @@ "value" : "Serveradresse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección del servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43522,6 +49006,12 @@ "value" : "Serverindstilling" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opción de servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43563,6 +49053,12 @@ "value" : "Indstil" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "conjunto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43609,6 +49105,12 @@ "value" : "Setze LoRa Region" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer región LoRa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43679,6 +49181,12 @@ "value" : "Indstil GPIO-bolerne for RXD og TXD." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure los pines GPIO para RXD y TXD." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43713,6 +49221,12 @@ }, "Set to current location" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer en la ubicación actual" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -43741,6 +49255,12 @@ "value" : "Indstiller det maksimale antal hop, standard er 3. At øge antallet af hop øger også belastningen og bør ske med forsigtighed. O hop-broadcast-beskeder vil ikke modtage ACKs." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establece el número máximo de saltos; el valor predeterminado es 3. El aumento de saltos también aumenta la congestión y debe usarse con cuidado. Los mensajes de difusión de O hop no recibirán ACK." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -43775,6 +49295,12 @@ }, "Sets the screen clock format to 12-hour." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establece el formato del reloj de la pantalla en 12 horas." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -43803,6 +49329,12 @@ "value" : "Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ajustes" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -43831,6 +49363,12 @@ "value" : "Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43908,6 +49446,12 @@ "value" : "Zweiundsiebzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setenta y dos horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43972,6 +49516,12 @@ }, "Share Contact QR" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir Contacto QR" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -44006,6 +49556,12 @@ "value" : "Standort teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir ubicación" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -44034,6 +49590,12 @@ "value" : "Kanal QR Code teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir código QR" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -44110,6 +49672,12 @@ "value" : "QR Code & Link teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir código QR y enlace" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44150,6 +49718,12 @@ "value" : "Teile deinen Standort in Echtzeit und koordiniere deine Gruppe mithilfe integrierter GPS-Funktionen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comparta su ubicación en tiempo real y mantenga a su grupo coordinado con funciones de GPS integradas." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -44178,6 +49752,12 @@ "value" : "Gemeinsamer Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave compartida" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44224,6 +49804,12 @@ "value" : "Meshtastic Kanäle teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir canales Meshtastic" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -44300,6 +49886,12 @@ "value" : "Kurzname" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre corto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44347,6 +49939,12 @@ "value" : "Kort Rækkevidde - Hurtig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44388,6 +49986,12 @@ "value" : "Kort rækkevidde - Langsom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44429,6 +50033,12 @@ "value" : "Kort rækkevidde - Turbo" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Turbo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44463,6 +50073,12 @@ }, "Show a confirmation dialog before performing the factory reset" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar un cuadro de diálogo de confirmación antes de realizar el restablecimiento de fábrica" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -44491,6 +50107,12 @@ "value" : "Zeige Alarme" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44537,6 +50159,12 @@ "value" : "Zeige Alarme" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44583,6 +50211,12 @@ "value" : "Zeige Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar nodos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44629,6 +50263,12 @@ "value" : "Zeige auf dem Gerätebildschirm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar en la pantalla del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44675,6 +50315,12 @@ "value" : "Zeige auf der Netzwerkkarte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar en el mapa de malla." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44716,6 +50362,12 @@ "value" : "Zeige Wegpunkte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar puntos de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44744,6 +50396,12 @@ }, "Shows information for the connected Lora radio. You can swipe left to disconnect the radio and long press to start the live activity." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muestra información de la radio Lora conectada. Puede deslizar hacia la izquierda para desconectar la radio y mantener presionada para iniciar la actividad en vivo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -44772,6 +50430,12 @@ "value" : "Herunterfahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apagar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44818,6 +50482,12 @@ "value" : "Knoten herunterfahren?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Cerrar el nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44864,6 +50534,12 @@ "value" : "Knoten herunterfahren?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Apagar el nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -44910,6 +50586,12 @@ "value" : "Herunterfahren bei Stromunterbruch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apagado por pérdida de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -44974,6 +50656,12 @@ "value" : "Signal %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Señal %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45021,6 +50709,12 @@ "value" : "Einfach" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sencillo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45092,6 +50786,12 @@ "value" : "Singapore 923 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Singapur 923MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45139,6 +50839,12 @@ "value" : "Sechs Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seis horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45216,6 +50922,12 @@ "value" : "Skifahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "esquiar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45262,6 +50974,12 @@ "value" : "Smart Position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición inteligente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45302,6 +51020,12 @@ "value" : "SNR" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45342,6 +51066,12 @@ "value" : "SNR %@ dB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@ dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45382,6 +51112,12 @@ "value" : "SNR %@dB" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45422,6 +51158,12 @@ "value" : "Jordfugtighed" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Humedad del suelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45462,6 +51204,12 @@ "value" : "Jordtemperatur" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatura del suelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45503,6 +51251,12 @@ "value" : "Angiver hvor længe den overvågede GPIO skal udlæse." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Especifica cuánto tiempo debe emitir el GPIO monitoreado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45543,6 +51297,12 @@ "value" : "Geschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45589,6 +51349,12 @@ "value" : "Geschwindigkeit %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45635,6 +51401,12 @@ "value" : "Geschwindigkeit: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45675,6 +51447,12 @@ "value" : "App-Entwicklung unterstützen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrollo de aplicaciones para patrocinadores" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -45703,6 +51481,12 @@ "value" : "Spredningsfaktor" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Factor de dispersión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -45749,6 +51533,12 @@ "value" : "SSID" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSID" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45820,6 +51610,12 @@ "value" : "Standard" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -45879,6 +51675,12 @@ "value" : "Standard dæmpet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar silenciado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45955,6 +51757,12 @@ "value" : "Start" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Empezar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46026,6 +51834,12 @@ "value" : "State Broadcast Interval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión estatal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46067,6 +51881,12 @@ "value" : "Überall in Verbindung bleiben" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manténgase conectado en cualquier lugar" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -46089,6 +51909,12 @@ "value" : "Gem og videresend" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenar y reenviar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46135,6 +51961,12 @@ "value" : "Konfigurer Opbevaring og Videreformidling" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenar y reenviar configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46182,6 +52014,12 @@ "value" : "Store & Forward-modulkonfiguration modtaget: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo Store & Forward recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46252,6 +52090,12 @@ "value" : "Lagre- og videresendelsesservere kræver en ESP32-enhed med PSRAM eller Linux Native" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los servidores de almacenamiento y reenvío requieren un dispositivo ESP32 con PSRAM o Linux Native." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46292,6 +52136,12 @@ "value" : "Abonneret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suscrito" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -46339,6 +52189,12 @@ "value" : "Undersystem" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subsistema" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46385,6 +52241,12 @@ "value" : "Successfully uploaded '%1$@' with %2$lld overlays" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "'%@' subido correctamente con superposiciones %lld" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -46413,6 +52275,12 @@ "value" : "Unterstützt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apoyado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46453,6 +52321,12 @@ "value" : "Understøttede I2C Connected- sensorer bliver automatisk genkendt: BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los sensores conectados I2C compatibles se detectarán automáticamente, los sensores son BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 y SHTC3." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46500,6 +52374,12 @@ "value" : "Tabel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "mesa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46541,6 +52421,12 @@ "value" : "Taiwan" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taiwán" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -46581,6 +52467,12 @@ "value" : "TAK" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46635,6 +52527,12 @@ "value" : "TAK Tracker" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rastreador TAK" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46681,6 +52579,12 @@ "value" : "Tager en Meshtastic-kanal-URL og gemmer kanalindstillingerne" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toma la URL de un canal Meshtastic y guarda la configuración del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -46715,6 +52619,12 @@ }, "Takes a Meshtastic contact URL and saves it to the nodes database" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toma una URL de contacto Meshtastic y la guarda en la base de datos de nodos" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -46743,6 +52653,12 @@ }, "Tap to enter emoji" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca para ingresar emoji" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -46765,6 +52681,12 @@ "value" : "Tapback Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tapback" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46841,6 +52763,12 @@ "value" : "Telemetrie (Sensoren)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telemetria" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46917,6 +52845,12 @@ "value" : "Telemetrie Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de telemetría" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46994,6 +52928,12 @@ "value" : "Telemetrie Modul Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de telemetría recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47071,6 +53011,12 @@ "value" : "Temp" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "temperatura" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47117,6 +53063,12 @@ "value" : "Temperatur" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatura" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47163,6 +53115,12 @@ "value" : "Zehn Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "diez minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47240,6 +53198,12 @@ "value" : "Zehn Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diez segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47316,6 +53280,12 @@ "value" : "Dritter Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador terciario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47369,6 +53339,12 @@ "value" : "Textnachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje de texto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47440,6 +53416,12 @@ "value" : "TFT-farvedisplays" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantallas TFT a todo color" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47481,6 +53463,12 @@ "value" : "Thailand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tailandia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47521,6 +53509,12 @@ "value" : "Den tid vi venter, før vi anser din pakke som færdig." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La cantidad de tiempo que debemos esperar antes de que consideremos que su paquete está listo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47561,6 +53555,12 @@ "value" : "Kompasretningen på skærmen uden for cirklen vil altid pege mod nord." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El rumbo de la brújula en la pantalla fuera del círculo siempre apuntará al norte." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47607,6 +53607,12 @@ "value" : "Der Taupunkt ist gerade %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El punto de rocío es %@ en este momento." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47647,6 +53653,12 @@ "value" : "Den hurtigste hastighed, som positionsopdateringer vil blive sendt med, hvis afstanden er over minimumsafstanden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lo más rápido que se enviarán las actualizaciones de posición si se ha cumplido la distancia mínima" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47693,6 +53705,12 @@ "value" : "Die letzten 4 Zeichen der MAC-Adresse des Geräts werden an den Kurznamen angehängt, um den BLE-Namen des Geräts festzulegen. Der Kurzname kann bis zu 4 Byte lang sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los últimos 4 de la dirección MAC del dispositivo se agregarán al nombre corto para configurar el nombre BLE del dispositivo. El nombre corto puede tener hasta 4 bytes de longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47739,6 +53757,12 @@ "value" : "Det maksimale tidsrum uden at noden sender sin position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El intervalo máximo que puede transcurrir sin que un nodo transmita una posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47779,6 +53803,12 @@ "value" : "Meshtastic Apple-apps understøtter firmwareversion %@ og derover." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las aplicaciones Meshtastic de Apple admiten la versión de firmware %@ y superior." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47825,6 +53855,12 @@ "value" : "Den mindste afstandsændring i meter, der skal overvejes for en smart positionsudsendelse." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El cambio mínimo de distancia en metros a considerar para una transmisión de posición inteligente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -47878,6 +53914,12 @@ "value" : "Das Paket ist zu groß" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El paquete es demasiado grande." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47954,6 +53996,12 @@ "value" : "Der erste öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública principal autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48006,6 +54054,12 @@ "value" : "Die Region, in der du deine Funkgeräte verwenden wirst." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La región donde utilizará sus radios." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48052,6 +54106,12 @@ "value" : "Rodemnet, der skal bruges til MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El tema raíz que se utilizará para MQTT." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48093,6 +54153,12 @@ "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las funciones de enrutador son solo para ubicaciones estratégicas, como cimas de montañas y torres con pocos nodos cercanos, no para uso en áreas urbanas. El uso inadecuado dañará su malla local." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -48121,6 +54187,12 @@ "value" : "Der zweite öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública secundaria autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48173,6 +54245,12 @@ "value" : "Status der LED (an/aus)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El estado del LED (encendido/apagado)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48219,6 +54297,12 @@ "value" : "Der dritte öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública terciaria autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48265,6 +54349,12 @@ "value" : "URL'en for kanalindstillingerne" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La URL para la configuración del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48299,6 +54389,12 @@ }, "The URL for the node to add" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La URL del nodo a agregar." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -48327,6 +54423,12 @@ }, "There has been no response to a request for device metadata via PKC admin for this node." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No ha habido respuesta a una solicitud de metadatos del dispositivo a través del administrador de PKC para este nodo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -48343,6 +54445,12 @@ }, "There is an issue with this contact's public key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hay un problema con la clave pública de este contacto." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -48361,6 +54469,12 @@ "comment" : "A paragraph below the title that explains what the user is about to do.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estas configuraciones %@" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -48378,6 +54492,12 @@ "value" : "Disse indstillinger vil %@ kanaler. Den nuværende LoRa-konfiguration vil blive erstattet, hvis der er betydelige ændringer i LoRa-konfigurationen, vil enheden genstarte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estas configuraciones serán los canales %@. La configuración LoRa actual será reemplazada; si hay cambios sustanciales en la configuración LoRa, el dispositivo se reiniciará" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48418,6 +54538,12 @@ "value" : "Dreißig Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -48495,6 +54621,12 @@ "value" : "Dreißig Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -48572,6 +54704,12 @@ "value" : "Sechsunddreissig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta y seis horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -48642,6 +54780,12 @@ "value" : "Denne samtale vil blive slettet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta conversación será eliminada." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48682,6 +54826,12 @@ "value" : "Dette kan tage et stykke tid. Svaret vil vises i rutesporingsloggen (trace route) for den node, det blev sendt til." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "This could take a while, response will appear in the trace route log for the node it was sent to." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48722,6 +54872,12 @@ "value" : "Denne enhed vil sende rækkeviddetestbeskeder ud med det valgte interval." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este dispositivo enviará mensajes de prueba de alcance en el intervalo seleccionado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48774,6 +54930,12 @@ "value" : "Diese Nachricht wurde höchstwahrscheinlich nicht übermittelt." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es probable que este mensaje no se haya entregado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48814,6 +54976,12 @@ "value" : "Noden understøtter ingen konfigurerbare moduler." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este nodo no admite ningún módulo configurable." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48854,6 +55022,12 @@ "value" : "Dette vil deaktivere fast position og fjerne den aktuelt indstillede position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto desactivará la posición fija y eliminará la posición establecida actualmente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48894,6 +55068,12 @@ "value" : "Dette vil sende en nuværende position fra din telefon og aktivere fast position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto enviará una posición actual desde su teléfono y habilitará la posición fija." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -48947,6 +55127,12 @@ "value" : "Drei Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tres horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49024,6 +55210,12 @@ "value" : "Drei Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tres segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49101,6 +55293,12 @@ "value" : "Daumen runter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pulgar hacia abajo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49178,6 +55376,12 @@ "value" : "Daumen hoch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pulgar arriba" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49254,6 +55458,12 @@ "value" : "Zeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tiempo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49300,6 +55510,12 @@ "value" : "Zeitstempel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marca de tiempo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49346,6 +55562,12 @@ "value" : "Zeitzone" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zona horaria" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49392,6 +55614,12 @@ "value" : "Zeitzone für Daten auf dem Gerätebildschirm und Log." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zona horaria para fechas en la pantalla del dispositivo y registro." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49438,6 +55666,12 @@ "value" : "Zeitlimit erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo de espera" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49514,6 +55748,12 @@ "value" : "Zeitstempel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marca de tiempo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49578,6 +55818,12 @@ }, "Timing and Overrides" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temporización y anulaciones" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -49601,6 +55847,12 @@ "value" : "TLS-kryptering aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "TLS habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49647,6 +55899,12 @@ "value" : "For at overholde privatlivslove som CCPA og GDPR undgår vi at dele præcise lokaliseringsdata. I stedet bruger vi anonymiseret eller omtrentlig (upræcis) lokaliseringsinformation for at beskytte dit privatliv." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para cumplir con las leyes de privacidad como CCPA y GDPR, evitamos compartir datos de ubicación exacta. En su lugar, utilizamos información de ubicación anónima o aproximada (imprecisa) para proteger su privacidad." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -49675,6 +55933,12 @@ }, "To Radio (TX): %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "A la radio (TX): %lld" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -49698,6 +55962,12 @@ "value" : "Emne: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temas: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49744,6 +56014,12 @@ "value" : "Total" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "totales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49784,6 +56060,12 @@ "value" : "Sum af personer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAX TOTALES" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49830,6 +56112,12 @@ "value" : "Rutesporing (trace route)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49864,6 +56152,12 @@ }, "Trace Route (in %@s)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento (en %@)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -49898,6 +56192,12 @@ "value" : "Rutesporingslog" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -49945,6 +56245,12 @@ "value" : "Traceroute Ergebnis: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitud de ruta de seguimiento devuelta: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50015,6 +56321,12 @@ "value" : "Rutesporing igangsat" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50055,6 +56367,12 @@ "value" : "Rutesporing (trace route) sendt til %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento enviada a %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50095,6 +56413,12 @@ "value" : "Rutesporing %@ blev ikke igangsat." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se envió la ruta de seguimiento a %@." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50135,6 +56459,12 @@ "value" : "Rutesporing (trace route) var begrænset af rate. Du kan højst sende en rutesporing én gang hvert halve minut." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trace Route tenía una tarifa limitada. Puede enviar una ruta de rastreo como máximo una vez cada treinta segundos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50175,6 +56505,12 @@ "value" : "Standorte verfolgen und teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguimiento y compartir ubicaciones" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -50198,6 +56534,12 @@ "value" : "Sporingsprogram" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rastreador" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -50244,6 +56586,12 @@ "value" : "Verkehr" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tráfico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50284,6 +56632,12 @@ "value" : "Transmitter data (txd) GPIO-pin" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmitir datos (txd) pin GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50324,6 +56678,12 @@ "value" : "Overførsel aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión habilitada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50370,6 +56730,12 @@ "value" : "Behandl dobbelttryk på understøttede accelerometre som et brugertastetryk." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Considere el doble toque en los acelerómetros compatibles como si el usuario presionara un botón." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50416,6 +56782,12 @@ "value" : "TriggerType" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo de disparador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50456,6 +56828,12 @@ "value" : "Triple Klik Ad Hoc Ping" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ping ad hoc de triple clic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50502,6 +56880,12 @@ "value" : "Erneut versuchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inténtalo de nuevo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50549,6 +56933,12 @@ "value" : "Zwölf Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doce horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50626,6 +57016,12 @@ "value" : "Vierundzwanzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "veinticuatro horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50702,6 +57098,12 @@ "value" : "Zwei Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50779,6 +57181,12 @@ "value" : "Zwei Minutes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50856,6 +57264,12 @@ "value" : "Zwei Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -50926,6 +57340,12 @@ "value" : "UDP-udsendelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión UDP" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -50967,6 +57387,12 @@ "value" : "Ukraine 433 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ucrania 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51008,6 +57434,12 @@ "value" : "Ukraine 868 MHz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ucrania 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51048,6 +57480,12 @@ "value" : "Fjern foretrukken" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No favorito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51089,6 +57527,12 @@ "value" : "Usund" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insalubre" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51130,6 +57574,12 @@ "value" : "Usundt for følsomme grupper" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No saludable para grupos sensibles" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51171,6 +57621,12 @@ "value" : "USA" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estados Unidos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51211,6 +57667,12 @@ "value" : "Enheder vist på enhedens skærm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidades mostradas en la pantalla del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51246,6 +57708,12 @@ "unknown" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "desconocido" + } + }, "it" : { "stringUnit" : { "state" : "needs_review", @@ -51286,6 +57754,12 @@ "value" : "Ukendt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconocido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -51362,6 +57836,12 @@ "value" : "Unbekanntes alter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edad desconocida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -51432,6 +57912,12 @@ "value" : "Nicht benachrichtigbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "inmensable" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -51454,6 +57940,12 @@ }, "Unmonitored" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No monitoreado" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -51488,6 +57980,12 @@ "value" : "Unset" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarmado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -51558,6 +58056,12 @@ "value" : "Ikke understøttet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No compatible" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51605,6 +58109,12 @@ "value" : "Hoch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "arriba" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -51675,6 +58185,12 @@ "value" : "Op Ned 1" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "arriba abajo 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51721,6 +58237,12 @@ "value" : "Opdateringsinterval" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de actualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51767,6 +58289,12 @@ "value" : "Firmware aktualisieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualice su firmware" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -51837,6 +58365,12 @@ "value" : "Opdaterede statistikker for noden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de estadísticas de nodos actualizados." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51883,6 +58417,12 @@ "value" : "Aktualisiert: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51923,6 +58463,12 @@ "value" : "Uplink aktiveret" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enlace ascendente habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -51969,6 +58515,12 @@ "value" : "Hochladen fehlgeschlagen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de carga" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52015,6 +58567,12 @@ "value" : "Lade GeoJSON-Dateien hoch, um eigene Karten-Overlays anzuzeigen. Die Dateien werden lokal gespeichert und dürfen bis zu 10 MB groß sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargue archivos GeoJSON para mostrar superposiciones de mapas personalizados. Los archivos se almacenan localmente y pueden tener hasta 10 MB." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52056,6 +58614,12 @@ "Upload Map Data" : { "comment" : "Title for map data upload screen", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar datos del mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52103,6 +58667,12 @@ "value" : "Lade Kartendaten hoch, um Overlays zu aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar datos de mapas para habilitar superposiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52149,6 +58719,12 @@ "value" : "Kartendaten hochladen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar superposiciones de mapas" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52171,6 +58747,12 @@ "value" : "Hochladen erfolgreich" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subir con éxito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52217,6 +58799,12 @@ "value" : "Hochgeladene Kartendaten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Superposiciones de mapas cargados" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52239,6 +58827,12 @@ "value" : "Oppetid" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tiempo de actividad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52285,6 +58879,12 @@ "value" : "Telemetriedaten erfassen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de uso y fallos" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52307,6 +58907,12 @@ "value" : "Brug en PWM-udgang (som RAK Buzzer) til melodier i stedet for en tænd/sluk-udgang. Dette vil ignorere udgang, udgangsvarighed og aktive indstillinger og bruge enhedens konfigurationsbuzzer-GPIO-option i stedet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice una salida PWM (como el RAK Buzzer) para melodías en lugar de una salida de encendido/apagado. Esto ignorará la salida, la duración de la salida y la configuración activa y en su lugar utilizará la opción GPIO del zumbador de configuración del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52353,6 +58959,12 @@ "value" : "Brug I2S som buzzer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice I2S como zumbador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52393,6 +59005,12 @@ "value" : "Standort verwenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar mi ubicación" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -52427,6 +59045,12 @@ "value" : "Voreinstellung verwenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar preajuste" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52473,6 +59097,12 @@ "value" : "Brug PWM-summer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar zumbador PWM" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52519,6 +59149,12 @@ "value" : "Verwende das GPS deines Handys anstelle des GPS deines Funkgeräts." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice el GPS de su teléfono para enviar ubicaciones a su nodo en lugar de utilizar un GPS de hardware en su nodo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52541,6 +59177,12 @@ "value" : "Bruges til at oprette en fælles krypteringsnøgle med en anden enhed." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se utiliza para crear una clave compartida con un dispositivo remoto." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52581,6 +59223,12 @@ "value" : "Wird verwendet, um nicht überwachte oder Infrastrukturknoten zu identifizieren, damit Nachrichten nicht an Knoten gesendet werden, die niemals antworten werden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se utiliza para identificar nodos de infraestructura o no supervisados, de modo que la mensajería no esté disponible para nodos que nunca responderán." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -52615,6 +59263,12 @@ "value" : "Benutzer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -52691,6 +59345,12 @@ "value" : "Benutzerkonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52743,6 +59403,12 @@ "value" : "Benutzerdaten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52789,6 +59455,12 @@ "value" : "Bruger-ID" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identificación de usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52829,6 +59501,12 @@ }, "User Info Exchange Failed" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error en el intercambio de información del usuario" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52839,6 +59517,12 @@ }, "User Info Sent" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Información de usuario enviada" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52849,6 +59533,12 @@ }, "User Privacy" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacidad del usuario" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -52866,6 +59556,12 @@ "value" : "Daten verfügbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuario subido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -52918,6 +59614,12 @@ "value" : "Benutzername" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de usuario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -52988,6 +59690,12 @@ "value" : "Bruger pullup-modstand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utiliza resistencia pullup" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53028,6 +59736,12 @@ "value" : "Udnytter netværksforbindelsen på din telefon til at oprette forbindelse til MQTT" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utiliza la conexión de red de su teléfono para conectarse a MQTT." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53080,6 +59794,12 @@ "value" : "Fahrzeugsteuerkurs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rumbo del vehículo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53126,6 +59846,12 @@ "value" : "Fahrzeuggeschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad del vehículo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53160,6 +59886,12 @@ }, "Verify who you are messaging with by comparing public keys in person or over the phone. The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again if the key change was due to a factory reset or other intentional action but this also may indicate a more serious security problem." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifique con quién está enviando mensajes comparando claves públicas en persona o por teléfono. The most recent public key for this node does not match the previously recorded key. Puede eliminar el nodo y dejar que intercambie claves nuevamente si el cambio de clave se debió a un restablecimiento de fábrica u otra acción intencional, pero esto también puede indicar un problema de seguridad más grave." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -53188,6 +59920,12 @@ "value" : "Version %1$@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %2$@ and above are supported." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La versión %@ incluye optimizaciones sustanciales de la red y cambios extensos en dispositivos y aplicaciones cliente. Solo se admiten los nodos versión %@ y superiores." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53240,6 +59978,12 @@ "value" : "Version: %1$@ (%2$@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión: %@ (%@)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -53281,6 +60025,12 @@ "value" : "Version: %1$@ (%2$@) " } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión: %1$@ (%2$@)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53328,6 +60078,12 @@ "value" : "Meget usund" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "muy poco saludable" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53374,6 +60130,12 @@ "value" : "Via Lora" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vía Lora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53420,6 +60182,12 @@ "value" : "Via Mqtt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vía Mqtt" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53472,6 +60240,12 @@ "value" : "Voltage" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "voltaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -53542,6 +60316,12 @@ "value" : "Volt %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltios %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53588,6 +60368,12 @@ "value" : "Warte..." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "esperando" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -53658,6 +60444,12 @@ "value" : "Afventer bekræftelse…" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esperando ser reconocido. . ." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53698,6 +60490,12 @@ "value" : "Væk skærmen ved tryk eller bevægelse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar pantalla con un toque o movimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53745,6 +60543,12 @@ "value" : "Gehen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caminando" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53798,6 +60602,12 @@ "value" : "Welle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ola" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53838,6 +60648,12 @@ }, "Waypoint Failed to Send" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El punto de referencia no se pudo enviar" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -53872,6 +60688,12 @@ "value" : "Wegpunktoptionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -53919,6 +60741,12 @@ "value" : "Wegpunkt von Knoten empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paquete de waypoint recibido del nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -53983,6 +60811,12 @@ }, "Waypoints" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puntos de ruta" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -54005,6 +60839,12 @@ "value" : "Wetterverhältnisse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condiciones climáticas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54051,6 +60891,12 @@ "value" : "Web Flasher" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intermitente web" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54091,6 +60937,12 @@ "value" : "Websted" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sitio web" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54137,6 +60989,12 @@ "value" : "Vægt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peso" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54177,6 +61035,12 @@ "value" : "Willkommen bei" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bienvenido a" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -54205,6 +61069,12 @@ "value" : "Was bedeutet das Schloß?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Qué significa la cerradura?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54257,6 +61127,12 @@ "value" : "Was ist Meshtastic?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Qué es Meshtastic?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54303,6 +61179,12 @@ "value" : "Hvad licenseret operatørtilstand gør:\n* Indstiller nodenavnet til dit kaldesignal \n* Udsender nodeinfo hvert 10. minut \n* Tilsidesætter frekvens, arbejdstidscyklus og sendeeffekt \n* Deaktiverer kryptering" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qué hace el modo de operador con licencia:\n* Establece el nombre del nodo según su indicativo de llamada \n* Transmite información del nodo cada 10 minutos \n* Anula la frecuencia, el ciclo de trabajo y la potencia de transmisión. \n* Desactiva el cifrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54349,6 +61231,12 @@ "value" : "Når det er aktiveret, tæller PAX Counter modulet antallet af personer, der passerer ved at bruge WiFi og Bluetooth. Både WiFi og Bluetooth skal være deaktiveret for at PAX counter kan fungere." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando está habilitado, el módulo Contador de PAX cuenta el número de personas que pasan mediante WiFi y Bluetooth. Tanto WiFI como Bluetooth deben estar desactivados para que funcione el contador de PAX." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -54407,6 +61295,12 @@ "value" : "Når du bruger i GPIO-tilstand, hold outputten tændt i så lang tid." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando lo use en modo GPIO, mantenga la salida encendida durante este tiempo. " + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54453,6 +61347,12 @@ "value" : "Om INPUT_PULLUP-tilstand skal bruges til GPIO-pin. Kun relevant hvis kortet bruger pull-up modstande på pinnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si se utiliza o no el modo INPUT_PULLUP para el pin GPIO. Only applicable if the board uses pull-up resistors on the pin" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54493,6 +61393,12 @@ "value" : "WiFi" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54545,6 +61451,12 @@ "value" : "WiFi Optionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones WiFi" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54585,6 +61497,12 @@ "value" : "Vil sove alt så meget som muligt, for tracker- og sensorrollen vil dette også omfatte lora-radioen. Brug ikke denne indstilling, hvis du vil bruge din enhed med telefonapps eller bruger en enhed uden en brugerknap." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dormirá todo lo más posible, para la función de rastreador y sensor esto también incluirá la radio lora. No use esta configuración si desea usar su dispositivo con las aplicaciones del teléfono o si está usando un dispositivo sin un botón de usuario." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -54649,6 +61567,12 @@ "value" : "Vind" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54696,6 +61620,12 @@ "value" : "Windrichtung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección del viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54743,6 +61673,12 @@ "value" : "Windgeschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad del viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54790,6 +61726,12 @@ "value" : "Innerhalb %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dentro de %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54836,6 +61778,12 @@ "value" : "x" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "x" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54882,6 +61830,12 @@ "value" : "X: %1$@, Y: %2$d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54935,6 +61889,12 @@ "value" : "X: %1$@, Y: %2$f" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$f" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -54988,6 +61948,12 @@ "value" : "X: %1$@, Y: %2$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55035,6 +62001,12 @@ "value" : "j" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "y" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55077,6 +62049,12 @@ "comment" : "A button label that appears in a confirmation sheet when favoriting a node as a CLIENT_BASE.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sí, controlo este nodo." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -55099,6 +62077,12 @@ "value" : "Gestern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ayer" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55139,6 +62123,12 @@ "value" : "Du kan også opdatere din Meshtastic-enhed over bluetooth ved hjælp af Nordic DFU-appen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "También puede actualizar su dispositivo Meshtastic a través de bluetooth utilizando la aplicación Nordic DFU." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55191,6 +62181,12 @@ "value" : "Du kannst Kanalnachrichten (Gruppenchats) und Direktnachrichten senden und empfangen. Bei jeder Nachricht kannst du lange drücken, um verfügbare Aktionen wie Kopieren, Antworten, Tapback und Löschen sowie Zustelldetails anzuzeigen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puedes enviar y recibir canales (chats grupales) y mensajes directos. Desde cualquier mensaje, puede mantener presionado para ver las acciones disponibles como copiar, responder, retroceder y eliminar, así como los detalles de entrega." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -55261,6 +62257,12 @@ "value" : "Din nuværende placering vil blive sat som den faste position og udsendt over nettet på positionsintervallet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su ubicación actual se establecerá como posición fija y se transmitirá sobre la malla en el intervalo de posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55313,6 +62315,12 @@ "value" : "Deine Firmware ist aktuell" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su firmware está actualizado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55359,6 +62367,12 @@ "value" : "Din MQTT-server skal understøtte TLS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su servidor MQTT debe admitir TLS." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55399,6 +62413,12 @@ "value" : "Din node vil med jævne mellemrum sende en ukrypteret kortrapportpakke til den konfigurerede MQTT-server, dette inkluderer id, kort og langt navn, omtrentlig placering, hardwaremodel, rolle, firmwareversion, LoRa-region, modemindstilling og primærkanalnavn." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su nodo enviará periódicamente un paquete de informe de mapa sin cifrar al servidor MQTT configurado, esto incluye identificación, nombre corto y largo, ubicación aproximada, modelo de hardware, función, versión de firmware, región LoRa, configuración predeterminada del módem y nombre del canal principal." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -55433,6 +62453,12 @@ "value" : "Din nodes driftsfrekvens beregnes baseret på regionen, modemforindstillingen og dette felt. Når det er 0, beregnes slot automatisk baseret på det primære kanals navn." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La frecuencia operativa de su nodo se calcula en función de la región, la configuración predeterminada del módem y este campo. Cuando es 0, la ranura se calcula automáticamente en función del nombre del canal principal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55473,6 +62499,12 @@ "value" : "Din position er blevet sendt med en anmodning om svar med deres position. Du vil modtage en besked, når en position er returneret." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su posición ha sido enviada con una solicitud de respuesta con su posición. Recibirá una notificación cuando se devuelva una posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55513,6 +62545,12 @@ }, "Your public key is generated from your private key and sent to other nodes on the mesh so they can compute a shared secret key with you." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su clave pública se genera a partir de su clave privada y se envía a otros nodos de la malla para que puedan calcular una clave secreta compartida con usted." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -55529,6 +62567,12 @@ "value" : "Din region har en %lld%% driftcyklus. MQTT anbefales ikke, når du er driftcyklusbegrænset, den ekstra trafik vil hurtigt overvælde dit LoRa-mesh." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su región tiene un ciclo de trabajo %lld%%. No se recomienda MQTT cuando tiene un ciclo de trabajo restringido, el tráfico adicional abrumará rápidamente su malla LoRa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55575,6 +62619,12 @@ "value" : "Din region har en %lld%% timebaseret driftscyklus, din radio vil stoppe med at sende pakker, når det når grænsen pr. time." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su región tiene un ciclo de trabajo por hora %lld%%, su radio dejará de enviar paquetes cuando alcance el límite por hora." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55615,6 +62665,12 @@ "value" : "Din rute-fil skal have både breddegrad og længdegrad kolonner og overskrifter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su archivo de ruta debe tener columnas y encabezados de Latitud y Longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -55649,6 +62705,12 @@ }, "Your user info has been sent with a request for a response with their user info." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su información de usuario se envió con una solicitud de respuesta con su información de usuario." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -55656,6 +62718,88 @@ } } } + }, + ": %@" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + } + }, + "shouldTranslate" : false + }, + ": %d" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + } + }, + "shouldTranslate" : false } }, "version" : "1.1" From 9ceb34f1d5ca92e737993185ef7a1d6e44345e74 Mon Sep 17 00:00:00 2001 From: axunes Date: Thu, 2 Apr 2026 11:05:23 -0400 Subject: [PATCH 3/7] fix typo in hop limit option description (#1631) O hop -> 0 hop --- Localizable.xcstrings | 2 +- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1083039f..948176b5 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -49247,7 +49247,7 @@ } } }, - "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : { + "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs." : { "localizations" : { "da" : { "stringUnit" : { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 87562617..17e18dc7 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -142,7 +142,7 @@ struct LoRaConfig: View { .tag($0) } } - Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs.") + Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.") .foregroundColor(.gray) .font(.callout) } From 2cabd9e575538696a448f93930f031bf34f55106 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:34:01 -0700 Subject: [PATCH 4/7] Tak server improvements (#1603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added read only mode cot to meshtastic parsing and warning to not enable on pub channel * better icons * fully fixed markers * telemetry support * Update Meshtastic/Helpers/TAK/TAKServerManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixes * fixes * Resolve merge conflicts for PR #1603 (TAK server improvements) (#1645) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B Co-authored-by: Garth Vander Houwen Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo Co-authored-by: axunes * Fix merge conflicts * Merge main into tak-server-improvements to resolve PR #1603 conflicts (#1646) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B Co-authored-by: Garth Vander Houwen Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo Co-authored-by: axunes * Merge main into tak-server-improvements to resolve PR #1603 conflicts (#1647) * Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B Co-authored-by: Garth Vander Houwen Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo Co-authored-by: axunes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jake-B Co-authored-by: Garth Vander Houwen Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo Co-authored-by: axunes --- Localizable.xcstrings | 171 ++++ .../Accessory Manager/AccessoryManager.swift | 38 + Meshtastic/Helpers/TAK/CoTMessage.swift | 6 +- .../Helpers/TAK/MeshToCoTConverter.swift | 271 +++++ .../Helpers/TAK/TAKMeshtasticBridge.swift | 940 +++++++++++++++++- Meshtastic/Helpers/TAK/TAKServerManager.swift | 304 +++++- .../Views/Settings/TAKServerConfig.swift | 141 +++ 7 files changed, 1827 insertions(+), 44 deletions(-) create mode 100644 Meshtastic/Helpers/TAK/MeshToCoTConverter.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 948176b5..30e6245e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -59,6 +59,90 @@ }, "shouldTranslate" : false }, + " : %@" : { + "extractionState" : "stale", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + } + }, + "shouldTranslate" : false + }, + " : %d" : { + "extractionState" : "stale", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + } + }, + "shouldTranslate" : false + }, " %@" : { "localizations" : { "da" : { @@ -149,6 +233,12 @@ "value" : " : %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -190,6 +280,12 @@ "value" : " : %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " : %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6611,6 +6707,10 @@ } } }, + "Auto-Fix Channel" : { + "comment" : "A button label that initiates the process of automatically fixing the TAK server's primary communication channel.", + "isCommentAutoGenerated" : true + }, "Automatically Connect" : { "localizations" : { "es" : { @@ -8179,6 +8279,10 @@ } } }, + "Bridge Meshtastic positions, nodes, waypoints, and messages to TAK/CoT format" : { + "comment" : "A description of the Mesh to CoT Converter feature.", + "isCommentAutoGenerated" : true + }, "Broadcast Device Metrics" : { "localizations" : { "es" : { @@ -10435,6 +10539,10 @@ } } }, + "Channel Fixed!" : { + "comment" : "A message displayed when the primary channel is successfully fixed.", + "isCommentAutoGenerated" : true + }, "Channel Name" : { "localizations" : { "da" : { @@ -16332,6 +16440,10 @@ } } }, + "Device role is \"%@\". Consider setting to TAK or TAK Tracker for optimal operation." : { + "comment" : "A warning about a device's role on the TAK network. The argument is the name of the device role.", + "isCommentAutoGenerated" : true + }, "Device Screen" : { "localizations" : { "da" : { @@ -19619,6 +19731,7 @@ "Enter P12 Password" : {}, "Enter the password for the PKCS#12 file" : {}, "environment" : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -22460,6 +22573,14 @@ } } }, + "Fix Channel" : { + "comment" : "The text on a button that, when pressed, will attempt to fix the primary LoRa channel.", + "isCommentAutoGenerated" : true + }, + "Fix Primary Channel?" : { + "comment" : "A confirmation alert title.", + "isCommentAutoGenerated" : true + }, "Fixed Pin" : { "localizations" : { "da" : { @@ -28268,6 +28389,10 @@ } } }, + "Later" : { + "comment" : "A button that dismisses an alert without taking any action.", + "isCommentAutoGenerated" : true + }, "Latitude" : { "localizations" : { "da" : { @@ -31199,6 +31324,10 @@ } } }, + "Mesh to CoT Converter" : { + "comment" : "A feature that bridges Meshtastic positions, nodes, waypoints, and messages to TAK/CoT format.", + "isCommentAutoGenerated" : true + }, "Meshtastic" : { "localizations" : { "es" : { @@ -31221,6 +31350,10 @@ } } }, + "Meshtastic -> TAK works, TAK -> Meshtastic blocked" : { + "comment" : "A description of the read-only mode feature in TAK Server.", + "isCommentAutoGenerated" : true + }, "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { "localizations" : { "es" : { @@ -37112,6 +37245,10 @@ } } }, + "Or fix it yourself in Channels settings, then return here." : { + "comment" : "A message explaining that the user can fix the primary channel settings manually and then return to the current view.", + "isCommentAutoGenerated" : true + }, "OS Log Entry Details" : { "localizations" : { "da" : { @@ -41768,6 +41905,10 @@ } } }, + "Read-Only Mode" : { + "comment" : "A toggle that allows the user to enable or disable read-only mode for the TAK server.", + "isCommentAutoGenerated" : true + }, "Reboot" : { "localizations" : { "da" : { @@ -46308,6 +46449,11 @@ } }, "Secure mTLS connection on port 8089. Both server and client certificates are required." : {}, + + "Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent." : { + "comment" : "A footer for the TAK Server configuration section.", + "isCommentAutoGenerated" : true + }, "Security" : { "localizations" : { "da" : { @@ -52510,6 +52656,7 @@ } } } + }, "TAK Server" : {}, "TAK Tracker" : { @@ -55014,6 +55161,10 @@ } } }, + "This will change your primary channel to:\n• Name: TAK\n• Encryption: New 256-bit AES key\n• LoRa preset: Short Fast (recommended for TAK)\n\nThis is required for TAK Server to work properly. Any existing channel sharing links will become invalid." : { + "comment" : "The message shown in the \"Fix Primary Channel?\" alert.", + "isCommentAutoGenerated" : true + }, "This will disable fixed position and remove the currently set position." : { "localizations" : { "da" : { @@ -58899,6 +59050,10 @@ } } }, + "Use a 256-bit encryption key" : { + "comment" : "A bullet point describing the importance of using a 256-bit encryption key for the primary channel.", + "isCommentAutoGenerated" : true + }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { "localizations" : { "da" : { @@ -60587,6 +60742,10 @@ } } }, + "Warning" : { + "comment" : "The header text for the \"Warning\" section in the TAKServerConfig view.", + "isCommentAutoGenerated" : true + }, "Wave" : { "extractionState" : "stale", "localizations" : { @@ -62167,6 +62326,10 @@ } } }, + "You can fix this yourself by changing your primary channel:" : { + "comment" : "A description of how to fix the primary channel in the TAK Server configuration view.", + "isCommentAutoGenerated" : true + }, "You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details." : { "localizations" : { "da" : { @@ -62249,6 +62412,10 @@ } } }, + "Your channel has been configured for TAK. To share the QR code: go to Settings > Share QR Code" : { + "comment" : "A message displayed when a user successfully configures their primary channel for TAK. It instructs the user to share the QR code to invite TAK buddies.", + "isCommentAutoGenerated" : true + }, "Your current location will be set as the fixed position and broadcast over the mesh on the position interval." : { "localizations" : { "da" : { @@ -62543,6 +62710,10 @@ } } }, + "Your primary channel is using the default settings (no name or default encryption key). TAK Server is running in read-only mode." : { + "comment" : "A description of a situation where the user's primary channel is not configured with a name or encryption key, and TAK Server is running in read-only mode.", + "isCommentAutoGenerated" : true + }, "Your public key is generated from your private key and sent to other nodes on the mesh so they can compute a shared secret key with you." : { "localizations" : { "es" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index cff4ab5a..5e1a46bd 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -512,12 +512,50 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { switch data.portnum { case .textMessageApp, .detectionSensorApp, .alertApp: await handleTextMessageAppPacket(packet) + // Broadcast text message to TAK clients + if let text = String(bytes: data.payload, encoding: .utf8) { + Logger.tak.debug("Text message received, calling broadcast") + let server = TAKServerManager.shared + if server.ensureBridgeReadyForMeshToCot() { + await server.bridge?.broadcastMeshTextMessageToTAK(text: text, from: packet.from, channel: packet.channel, to: packet.to) + } + } case .remoteHardwareApp: Logger.mesh.info("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .positionApp: await MeshPackets.shared.upsertPositionPacket(packet: packet) + // Broadcast position to TAK clients + if let position = try? Position(serializedBytes: data.payload) { + Logger.tak.debug("Position received, calling broadcast") + let server = TAKServerManager.shared + if server.ensureBridgeReadyForMeshToCot() { + await server.bridge?.broadcastMeshPositionToTAK(position: position, from: packet.from) + } + } case .waypointApp: + Logger.tak.info("WAYPOINT APP CASE REACHED") await MeshPackets.shared.waypointPacket(packet: packet) + // Broadcast waypoint to TAK clients + if let waypoint = try? Waypoint(serializedBytes: data.payload) { + Logger.tak.info("WAYPOINT PARSED: \(waypoint.name)") + // Ensure bridge is initialized before calling (not optional chaining, or lazy init won't run) + let server = TAKServerManager.shared + if server.meshToCotEnabled && server.isRunning && !server.connectedClients.isEmpty { + // Force bridge initialization if needed + if server.bridge == nil { + Logger.tak.info("Initializing bridge on demand") + let bridge = TAKMeshtasticBridge( + accessoryManager: AccessoryManager.shared, + takServerManager: server + ) + bridge.context = AccessoryManager.shared.context + server.bridge = bridge + } + await server.bridge?.broadcastMeshWaypointToTAK(waypoint: waypoint, from: packet.from) + } else { + Logger.tak.info("Waypoint broadcast skipped: server not ready or no clients") + } + } case .nodeinfoApp: guard let connectedNodeNum = self.activeDeviceNum else { Logger.mesh.error("🕸️ Unable to determine connectedNodeNum for node info upsert.") diff --git a/Meshtastic/Helpers/TAK/CoTMessage.swift b/Meshtastic/Helpers/TAK/CoTMessage.swift index 12aff014..68a6b063 100644 --- a/Meshtastic/Helpers/TAK/CoTMessage.swift +++ b/Meshtastic/Helpers/TAK/CoTMessage.swift @@ -131,7 +131,8 @@ struct CoTMessage: Identifiable, Sendable { team: String = "Cyan", role: String = "Team Member", battery: Int = 100, - staleMinutes: Int = 10 + staleMinutes: Int = 10, + remarks: String? = nil ) -> CoTMessage { let now = Date() return CoTMessage( @@ -149,7 +150,8 @@ struct CoTMessage: Identifiable, Sendable { contact: CoTContact(callsign: callsign, endpoint: "0.0.0.0:4242:tcp"), group: CoTGroup(name: team, role: role), status: CoTStatus(battery: battery), - track: CoTTrack(speed: speed, course: course) + track: CoTTrack(speed: speed, course: course), + remarks: remarks ) } diff --git a/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift b/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift new file mode 100644 index 00000000..6c9f9029 --- /dev/null +++ b/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift @@ -0,0 +1,271 @@ +// +// MeshToCoTConverter.swift +// Meshtastic +// +// Converts Meshtastic packets to CoT format for TAK Server +// + +import Foundation +import MeshtasticProtobufs +import CoreLocation +import OSLog +import Combine + +/// Converts Meshtastic packets to CoT format for bridging to TAK Server +final class MeshToCoTConverter: ObservableObject { + + static let shared = MeshToCoTConverter() + + private let logger = Logger(subsystem: "Meshtastic", category: "MeshToCoT") + + private init() {} + + // MARK: - Position // MARK: Packet to CoT + + /// Convert a Meshtastic position packet to CoT message + func convertPosition(_ position: Position, from node: NodeInfoEntity) -> CoTMessage? { + guard let user = node.user else { + logger.warning("Cannot convert position: node has no user info") + return nil + } + + let callsign = user.longName ?? user.shortName ?? "Unknown" + let uid = "MESHTASTIC-\(node.num.toHex())" + + let latitude = Double(position.latitudeI) / 1e7 + let longitude = Double(position.longitudeI) / 1e7 + let altitude = Double(position.altitude) + + var speed: Double = 0 + var course: Double = 0 + if position.speed != 0 { + speed = Double(position.speed) * 0.194384 // Convert to knots + } + if position.heading != 0 { + course = Double(position.heading) + } + + let battery = Int(position.batteryLevel) + + return CoTMessage.pli( + uid: uid, + callsign: callsign, + latitude: latitude, + longitude: longitude, + altitude: altitude, + speed: speed, + course: course, + team: "Meshtastic", + role: "Team Member", + battery: battery > 0 ? battery : 100, + staleMinutes: 10 + ) + } + + // MARK: - Node Info to CoT + + /// Convert node info to CoT message (for node presence updates) + func convertNodeInfo(_ node: NodeInfoEntity) -> CoTMessage? { + guard let user = node.user else { + logger.warning("Cannot convert node info: node has no user info") + return nil + } + + let callsign = user.longName ?? user.shortName ?? "Unknown" + let uid = "MESHTASTIC-\(node.num.toHex())" + + var latitude = 0.0 + var longitude = 0.0 + var altitude = 9999999.0 + + if let position = node.position { + latitude = Double(position.latitudeI) / 1e7 + longitude = Double(position.longitudeI) / 1e7 + if position.altitude != 0 { + altitude = Double(position.altitude) + } + } + + // Determine CoT type based on device role + let cotType = getCoTTypeForRole(user.role) + + let now = Date() + return CoTMessage( + uid: uid, + type: cotType, + time: now, + start: now, + stale: now.addingTimeInterval(3600), // 1 hour stale for node info + how: "m-g", + latitude: latitude, + longitude: longitude, + hae: altitude, + ce: 9999999.0, + le: 9999999.0, + contact: CoTContact(callsign: callsign, endpoint: "0.0.0.0:4242:tcp"), + group: CoTGroup(name: "Meshtastic", role: getRoleNameForDeviceRole(user.role)), + remarks: "Meshtastic Node: \(callsign)" + ) + } + + // MARK: - Waypoint to CoT + + /// Convert a Meshtastic waypoint to CoT message + func convertWaypoint(_ waypoint: Waypoint, from node: NodeInfoEntity?) -> CoTMessage? { + let uid = "WAYPOINT-\(waypoint.id)" + + let latitude = Double(waypoint.latitudeI) / 1e7 + let longitude = Double(waypoint.longitudeI) / 1e7 + let altitude = waypoint.altitude > 0 ? Double(waypoint.altitude) : 9999999.0 + + let name = waypoint.name.isEmpty ? "Unnamed Waypoint" : waypoint.name + let description = waypoint.description_p.isEmpty ? "Meshtastic Waypoint" : waypoint.description_p + + // Get emoji based on waypoint icon/expire time + let iconEmoji = getEmojiForWaypoint(waypoint) + + // Handle expiry - if expire is 0, never expire. Otherwise use the expire time as Unix timestamp + let stale: Date + if waypoint.expire == 0 { + // Never expire - set to 1 year from now + stale = Date().addingTimeInterval(365 * 24 * 60 * 60) + } else { + // expire is Unix timestamp when waypoint expires + let expireDate = Date(timeIntervalSince1970: TimeInterval(waypoint.expire)) + if expireDate > Date() { + stale = expireDate + } else { + // Already expired, don't broadcast + return nil + } + } + + return CoTMessage( + uid: uid, + type: "b-ttf-ff", // Point feature friend - standard CoT type for waypoints/markers + time: Date(), + start: Date(), + stale: stale, + how: "m-g", + latitude: latitude, + longitude: longitude, + hae: altitude, + ce: 100.0, + le: 100.0, + contact: CoTContact(callsign: "\(iconEmoji) \(name)", endpoint: "0.0.0.0:4242:tcp"), + remarks: "\(description)\nCreated by: \(node?.user?.longName ?? "Unknown")" + ) + } + + // MARK: - Text Message to CoT + + /// Convert a Meshtastic text message to CoT chat message + func convertTextMessage(_ message: MessageEntity, from sender: NodeInfoEntity) -> CoTMessage? { + guard let user = sender.user, + let text = message.text else { + return nil + } + + let senderName = user.longName ?? user.shortName ?? "Unknown" + let senderUid = "MESHTASTIC-\(sender.num.toHex())" + let messageId = "MSG-\(message.id)" + + return CoTMessage.chat( + senderUid: senderUid, + senderCallsign: senderName, + message: text, + chatroom: "Primary" + ) + } + + // MARK: - Helper Methods + + /// Get CoT type based on device role + private func getCoTTypeForRole(_ role: UInt32) -> String { + switch DeviceRoles(rawValue: Int(role)) { + case .router, .routerLate: + return "a-f-G-E" // Group entity (router) + case .tracker: + return "a-f-G-T-C" // Ground unit tracker + case .tak: + return "a-f-G-U-C" // TAK client + case .takTracker: + return "a-f-G-T-C" // TAK tracker + case .sensor: + return "a-f-G-s" // Sensor with friendly affiliation + case .client, .clientMute, .clientHidden, .lostAndFound: + return "a-f-G-U-C" // Friendly ground unit + default: + return "a-f-G-U-C" // Default to friendly unit + } + } + + /// Get role name for device role + private func getRoleNameForDeviceRole(_ role: UInt32) -> String { + switch DeviceRoles(rawValue: Int(role)) { + case .router, .routerLate: + return "Router" + case .tracker: + return "Tracker" + case .tak: + return "TAK" + case .takTracker: + return "TAK Tracker" + case .sensor: + return "Sensor" + case .client: + return "Client" + case .clientMute: + return "Muted" + case .clientHidden: + return "Hidden" + default: + return "User" + } + } + + /// Get emoji for waypoint based on icon + private func getEmojiForWaypoint(_ waypoint: Waypoint) -> String { + // Use icon field if available, otherwise use expire time to guess + if waypoint.icon != 0 { + switch waypoint.icon { + case 1: return "📍" // Marker + case 2: return "🚗" // Car + case 3: return "🚶" // Person + case 4: return "🏠" // Home + case 5: return "⛺" // Camp + case 6: return "⚠️" // Warning + case 7: return "🏁" // Flag + case 8: return "🔍" // Search + case 9: return "🏥" // Medical + case 10: return "🔥" // Fire + case 11: return "🚁" // Helicopter + case 12: return "⛵" // Boat + case 13: return "🛸" // UFO + default: return "📍" + } + } + + // Fallback based on name + let name = waypoint.name.lowercased() + if name.contains("help") || name.contains("emergency") { + return "🆘" + } else if name.contains("medical") || name.contains("hospital") { + return "🏥" + } else if name.contains("danger") || name.contains("warning") { + return "⚠️" + } else if name.contains("camp") { + return "⛺" + } else if name.contains("home") || name.contains("house") { + return "🏠" + } else if name.contains("car") || name.contains("vehicle") { + return "🚗" + } else if name.contains("flag") { + return "🏁" + } else if name.contains("person") || name.contains("me") { + return "🚶" + } else { + return "📍" + } + } +} diff --git a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift index 23a08afe..8985accf 100644 --- a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift +++ b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift @@ -137,6 +137,16 @@ final class TAKMeshtasticBridge { /// Send a CoT message received from TAK to the Meshtastic mesh func sendToMesh(_ cotMessage: CoTMessage) async { + guard let takServerManager else { + Logger.tak.warning("Cannot send to mesh: TAKServerManager not available") + return + } + + guard !takServerManager.userReadOnlyMode else { + Logger.tak.info("TAK Server in read-only mode: Ignoring message from TAK client") + return + } + guard let accessoryManager else { Logger.tak.warning("Cannot send to mesh: AccessoryManager not available") return @@ -452,11 +462,37 @@ final class TAKMeshtasticBridge { } let uid = "MESHTASTIC-\(String(format: "%08X", node.num))" - let callsign = node.user?.shortName ?? node.user?.longName ?? "MESH-\(node.num)" - - // Get battery level from device metrics - let battery = Int(node.latestDeviceMetrics?.batteryLevel ?? 100) + // Format: "SHORT - Long Name" or just "ShortName" if no long name + let callsign: String + if let shortName = node.user?.shortName, let longName = node.user?.longName, !longName.isEmpty { + callsign = "\(shortName) - \(longName)" + } else { + callsign = node.user?.shortName ?? node.user?.longName ?? "MESH-\(node.num)" + } + // Get telemetry from device metrics + let deviceMetrics = node.latestDeviceMetrics + let battery = Int(deviceMetrics?.batteryLevel ?? 100) + let voltage = deviceMetrics?.voltage ?? 0 + let channelUtil = deviceMetrics?.channelUtilization ?? 0 + let rssi = deviceMetrics?.rssi ?? 0 + let snr = deviceMetrics?.snr ?? 0 + + // Build remarks with telemetry info + var remarks = "Battery: \(battery)%" + if voltage > 0 { + remarks += " | Voltage: \(String(format: "%.2f", voltage))V" + } + if channelUtil > 0 { + remarks += " | Chan Util: \(String(format: "%.1f", channelUtil))%" + } + if rssi != 0 { + remarks += " | RSSI: \(rssi) dBm" + } + if snr != 0 { + remarks += " | SNR: \(String(format: "%.1f", snr)) dB" + } + return CoTMessage.pli( uid: uid, callsign: callsign, @@ -468,7 +504,8 @@ final class TAKMeshtasticBridge { team: "Green", // Meshtastic nodes shown as green by default role: "Team Member", battery: battery, - staleMinutes: 15 // Meshtastic positions can be older + staleMinutes: 15, // Meshtastic positions can be older + remarks: remarks ) } @@ -476,24 +513,78 @@ final class TAKMeshtasticBridge { /// Send all known mesh node positions to TAK clients /// Useful when a new TAK client connects + /// Only sends nodes with positions updated within the last 2 hours + /// Excludes the node we're currently connected to func broadcastAllNodesToTAK() async { guard let takServerManager, takServerManager.isRunning else { return } - guard let context else { return } - + + // Get context - try the bridge's context first, then fall back to PersistenceController + let context = self.context ?? PersistenceController.shared.container.viewContext + + let twoHoursAgo = Date().addingTimeInterval(-7200) + + // Get the connected node number to exclude it + let connectedNodeNum = AccessoryManager.shared.activeDeviceNum ?? 0 + + Logger.tak.info("Starting broadcast of all mesh nodes to TAK (excluding node \(connectedNodeNum))") + + // Fetch all nodes - be more lenient, include any node that's been heard from + // We'll check positions when creating CoT messages let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() - // Only nodes with valid positions - fetchRequest.predicate = NSPredicate(format: "latestPosition != nil") - + fetchRequest.predicate = NSPredicate( + format: "user != nil" + ) + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastHeard", ascending: false)] + do { let nodes = try context.fetch(fetchRequest) - + Logger.tak.info("Found \(nodes.count) total nodes with user info, connected node: \(connectedNodeNum)") + + var broadcastCount = 0 + var skippedNoPosition = 0 + var skippedConnected = 0 + var skippedInvalidPosition = 0 + var skippedTooOld = 0 + for node in nodes { + // Skip the connected node - it's our own device + // Use the same pattern as other parts of the codebase: node.num == accessoryManager.activeDeviceNum + if node.num == connectedNodeNum { + Logger.tak.info("Skipping connected node \(node.num)") + skippedConnected += 1 + continue + } + + // Get position - use the extension's latestPosition computed property + guard let position = node.latestPosition, + let latitude = position.latitude, + let longitude = position.longitude else { + skippedNoPosition += 1 + continue + } + + // Skip nodes with invalid positions (0,0) + guard latitude != 0 || longitude != 0 else { + skippedInvalidPosition += 1 + continue + } + + // Check if node has been heard from recently (within last 2 hours) + if let lastHeard = node.lastHeard, lastHeard < twoHoursAgo { + skippedTooOld += 1 + continue + } + if let cotMessage = createCoTFromNode(node) { await takServerManager.broadcast(cotMessage) + broadcastCount += 1 + + // Small delay to avoid flooding the client + try? await Task.sleep(nanoseconds: 10_000_000) // 10ms } } - Logger.tak.info("Broadcast \(nodes.count) mesh node positions to TAK clients") + Logger.tak.info("Broadcast complete: \(broadcastCount) nodes sent, \(skippedConnected) skipped (connected), \(skippedNoPosition) skipped (no position), \(skippedInvalidPosition) skipped (invalid position), \(skippedTooOld) skipped (too old)") } catch { Logger.tak.error("Failed to fetch nodes for TAK broadcast: \(error.localizedDescription)") } @@ -502,10 +593,12 @@ final class TAKMeshtasticBridge { // MARK: - Helper Methods private func lookupNodeInfo(nodeNum: UInt32) -> NodeInfoEntity? { - guard let context else { return nil } + // Use PersistenceController's viewContext directly to ensure we can find nodes + let context = PersistenceController.shared.container.viewContext + // Use the same format as MeshPackets - num is Int64 let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "num == %d", Int64(nodeNum)) + fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) fetchRequest.fetchLimit = 1 do { @@ -515,4 +608,823 @@ final class TAKMeshtasticBridge { return nil } } + + // MARK: - Mesh to CoT Broadcasting + + /// Broadcast a Meshtastic position packet to connected TAK clients + /// Called when a new position is received from the mesh + func broadcastMeshPositionToTAK(position: Position, from nodeNum: UInt32) async { + // Lazy initialization of bridge if needed + if TAKServerManager.shared.bridge == nil { + Logger.tak.info("Initializing bridge lazily for position broadcast") + let bridge = TAKMeshtasticBridge( + accessoryManager: AccessoryManager.shared, + takServerManager: TAKServerManager.shared + ) + bridge.context = AccessoryManager.shared.context + TAKServerManager.shared.bridge = bridge + } + + let server = TAKServerManager.shared + guard server.meshToCotEnabled, server.isRunning else { return } + guard server.connectedClients.isEmpty == false else { return } + + guard let node = lookupNodeInfo(nodeNum: nodeNum) else { return } + + if let cotMessage = createCoTFromNode(node) { + await server.broadcast(cotMessage) + Logger.tak.info("Broadcast mesh position to TAK: \(node.user?.longName ?? "Unknown")") + } + } + + /// Broadcast a Meshtastic text message to connected TAK clients + /// Called when a text message is received from the mesh + /// - Parameters: + /// - text: The message text + /// - from: The sender node number + /// - channel: The channel index + /// - to: The destination node number (UInt32.max for broadcast) + func broadcastMeshTextMessageToTAK(text: String, from nodeNum: UInt32, channel: UInt32, to destination: UInt32) async { + // Lazy initialization of bridge if needed + if TAKServerManager.shared.bridge == nil { + Logger.tak.info("Initializing bridge lazily for text message broadcast") + let bridge = TAKMeshtasticBridge( + accessoryManager: AccessoryManager.shared, + takServerManager: TAKServerManager.shared + ) + bridge.context = AccessoryManager.shared.context + TAKServerManager.shared.bridge = bridge + } + + let server = TAKServerManager.shared + guard server.meshToCotEnabled, server.isRunning else { return } + guard server.connectedClients.isEmpty == false else { return } + + guard let node = lookupNodeInfo(nodeNum: nodeNum), + let user = node.user else { return } + + let senderName = user.longName ?? user.shortName ?? "Unknown" + let uid = "MSG-\(nodeNum)-\(Int(Date().timeIntervalSince1970))" + + // Determine if this is a DM or broadcast + let isDirectMessage = destination != UInt32.max + + // For now, send all messages to general chat but mark DMs in the message + let chatroom = "All Chat Rooms" + + Logger.tak.info("Text message: isDM=\(isDirectMessage), chatroom=\(chatroom), from=\(senderName)") + + let senderUid = "MESHTASTIC-\(String(format: "%08X", nodeNum))" + + // Prefix DM messages with "DM:" so users know it's a direct message + let messageText = isDirectMessage ? "DM: \(text)" : text + + let cotMessage = CoTMessage( + uid: "GeoChat.\(senderUid).\(chatroom.replacingOccurrences(of: " ", with: "_")).\(uid)", + type: "b-t-f", + time: Date(), + start: Date(), + stale: Date().addingTimeInterval(86400), + how: "h-g-i-g-o", + latitude: 0, + longitude: 0, + hae: 9999999.0, + ce: 9999999.0, + le: 9999999.0, + contact: CoTContact(callsign: senderName, endpoint: "0.0.0.0:4242:tcp"), + chat: CoTChat( + message: messageText, + senderCallsign: senderName, + chatroom: chatroom + ), + remarks: messageText + ) + + await server.broadcast(cotMessage) + Logger.tak.info("Broadcast mesh text message to TAK: \(senderName) to \(chatroom)") + } + + /// Broadcast a Meshtastic waypoint to connected TAK clients + /// Called when a waypoints is received from the mesh + func broadcastMeshWaypointToTAK(waypoint: Waypoint, from nodeNum: UInt32) async { + // Lazy initialization of bridge if needed - set on singleton + if TAKServerManager.shared.bridge == nil { + Logger.tak.info("Initializing bridge lazily on singleton") + let bridge = TAKMeshtasticBridge( + accessoryManager: AccessoryManager.shared, + takServerManager: TAKServerManager.shared + ) + bridge.context = AccessoryManager.shared.context + TAKServerManager.shared.bridge = bridge + } + + let server = TAKServerManager.shared + Logger.tak.info("Waypoint broadcast check: meshToCot=\(server.meshToCotEnabled), isRunning=\(server.isRunning), clients=\(server.connectedClients.count)") + + guard server.meshToCotEnabled, server.isRunning else { + Logger.tak.warning("Waypoint broadcast skipped: server not ready") + return + } + guard let context, server.connectedClients.isEmpty == false else { + Logger.tak.warning("Waypoint broadcast skipped: no clients") + return + } + + let node = lookupNodeInfo(nodeNum: nodeNum) + Logger.tak.info("Node lookup for \(nodeNum) (0x\(String(format: "%08X", nodeNum))): \(node != nil ? "found" : "NOT FOUND")") + if let node = node { + Logger.tak.info(" Node user: \(node.user?.longName ?? "nil"), shortName: \(node.user?.shortName ?? "nil")") + } + let senderName = node?.user?.longName ?? node?.user?.shortName ?? "Unknown Node" + + let uid = "WAYPOINT-\(waypoint.id)" + let latitude = Double(waypoint.latitudeI) / 1e7 + let longitude = Double(waypoint.longitudeI) / 1e7 + + let name = waypoint.name.isEmpty ? "Dropped Pin" : waypoint.name + let description = waypoint.description_p.isEmpty ? "Meshtastic Waypoint" : waypoint.description_p + + Logger.tak.info("Broadcasting waypoint: \(name) at \(latitude), \(longitude), sender: \(senderName)") + + // Map Meshtastic emoji icon to appropriate TAK icon + let (cotType, iconPath, colorArgb) = getTakIconForWaypoint(waypoint: waypoint) + let userIconXML = "" + Logger.tak.info("Waypoint icon: emoji=0x\(String(format: "%08X", waypoint.icon)) -> \(iconPath)") + + // Handle expiry - if expire is 0, never expire. Otherwise use the expire time + let stale: Date + if waypoint.expire == 0 { + // Never expire - set to 1 year from now + stale = Date().addingTimeInterval(365 * 24 * 60 * 60) + Logger.tak.info("Waypoint set to never expire") + } else { + // expire is Unix timestamp when waypoint expires + let expireDate = Date(timeIntervalSince1970: TimeInterval(waypoint.expire)) + if expireDate > Date() { + stale = expireDate + } else { + // Already expired, don't broadcast + Logger.tak.warning("Waypoint already expired, skipping broadcast") + return + } + } + + // Include the usericon in the detail (no color to avoid background in TAKware) + let rawDetail = "\(userIconXML)" + + let cotMessage = CoTMessage( + uid: uid, + type: cotType, + time: Date(), + start: Date(), + stale: stale, + how: "m-g", + latitude: latitude, + longitude: longitude, + hae: 0, + ce: 10.0, + le: 10.0, + contact: CoTContact(callsign: "\(name) - \(senderName)", endpoint: "0.0.0.0:4242:tcp"), + remarks: "\(description)\nFrom: \(senderName) [\(String(format: "%08X", nodeNum))]", + rawDetailXML: rawDetail + ) + + await server.broadcast(cotMessage) + Logger.tak.info("Broadcast mesh waypoint to TAK: \(name) from \(senderName)") + } + + /// Map Meshtastic waypoint emoji to TAK icon + /// Returns (cotType, iconPath, colorArgb) + /// Icon paths use format: UUID/Category/icon.png + /// Priority: Google > Generic Icons (fallback) + private func getTakIconForWaypoint(waypoint: Waypoint) -> (String, String, String) { + let icon = waypoint.icon + + // Icon set UUIDs + let googleUUID = "f7f71666-8b28-4b57-9fbb-e38e61d33b79" + let genericUUID = "ad78aafb-83a6-4c07-b2b9-a897a8b6a38f" + + switch icon { + // 📍 📌 Pushpin - RED pushpin (default) + case 0x1F4CD, 0x1F4CC, 1: // 📍 📌 + return ("a-u-G", "\(genericUUID)/Tacks/red-pushpin.png", "-16776961") + + // === EMERGENCY === + // 🔥 Fire - Google firedept + case 0x1F525, 10: // 🔥 + return ("a-u-G", "\(googleUUID)/Google/firedept.png", "-16776961") + // 🚨 Siren - Google caution + case 0x1F6A8, 6: // 🚨 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-256") + // 🏥 Hospital - Google hospitals + case 0x1F3E5, 0x2695, 9: // 🏥 ➕ + return ("a-u-G", "\(googleUUID)/Google/hospitals.png", "-16776961") + // 🚑 Ambulance - Google hospitals (no ambulance in Google) + case 0x1F691: // 🚑 + return ("a-u-G", "\(googleUUID)/Google/hospitals.png", "-16776961") + // ⚠️ Warning - Google caution + case 0x26A0: // ⚠️ + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-256") + // 🚓 Police - Google police + case 0x1F693: // 🚓 + return ("a-u-G", "\(googleUUID)/Google/police.png", "-16776961") + // 🏃 Runner - Google man + case 0x1F3C3: // 🏃 + return ("a-u-G", "\(googleUUID)/Google/man.png", "-16711936") + // 💀 Skull - Google caution + case 0x1F480: // 💀 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-1") + // 💣 Bomb - Google caution + case 0x1F4A3: // 💣 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + + // === TRANSPORT === + // 🚗 Car - Google bus (closest) + case 0x1F697, 0x1F695, 2: // 🚗 🚕 + return ("a-u-G", "\(googleUUID)/Google/bus.png", "-256") + // 🚁 Helicopter - Google heliport + case 0x1F681, 11: // 🚁 + return ("a-u-G", "\(googleUUID)/Google/heliport.png", "-16776961") + // ⛵ Boat - Google marina + case 0x26F5, 12: // ⛵ + return ("a-u-G", "\(googleUUID)/Google/marina.png", "-16776961") + // 🚢 Ship - Google marina + case 0x1F6A2: // 🚢 + return ("a-u-G", "\(googleUUID)/Google/marina.png", "-16776961") + // 🚀 Rocket - Google target + case 0x1F680: // 🚀 + return ("a-u-G", "\(googleUUID)/Google/target.png", "-16776961") + // 🛸 UFO - Generic purple pushpin + case 0x1F6B8, 13: // 🛸 + return ("a-u-G", "\(genericUUID)/Tacks/purple-pushpin.png", "-65281") + // 🚲 Bicycle - Google cycling + case 0x1F6B2: // 🚲 + return ("a-u-G", "\(googleUUID)/Google/cycling.png", "-16711936") + // 🚆 Train - Google rail + case 0x1F686: // 🚆 + return ("a-u-G", "\(googleUUID)/Google/rail.png", "-16711936") + // ✈️ Plane - Google airports + case 0x2708: // ✈️ + return ("a-u-G", "\(googleUUID)/Google/airports.png", "-16776961") + // 🚛 Truck - Google bus + case 0x1F69A: // 🚛 + return ("a-u-G", "\(googleUUID)/Google/bus.png", "-16711936") + // 🚌 Bus - Google bus + case 0x1F68C: // 🚌 + return ("a-u-G", "\(googleUUID)/Google/bus.png", "-256") + + // === PLACES === + // 🏨 Hotel - Google lodging + case 0x1F3E8: // 🏨 + return ("a-u-G", "\(googleUUID)/Google/lodging.png", "-16776961") + // 🏪 Store - Google convenience + case 0x1F3EA: // 🏪 + return ("a-u-G", "\(googleUUID)/Google/convenience.png", "-16711936") + // ⛽ Gas - Google gas_stations + case 0x1F6FD: // ⛽ + return ("a-u-G", "\(googleUUID)/Google/gas_stations.png", "-16776961") + // 🏰 Castle - Google info + case 0x1F3F0: // 🏰 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🏛️ Government - Google info + case 0x1F3DB: // 🏛️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // ⛲ Fountain - Generic fountain (use info) + case 0x1F6F1: // ⛲ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🏞️ Park - Google parks + case 0x1F3DE: // 🏞️ + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16711936") + + // === PEOPLE === + // 🚶 Person - Google hiker + case 0x1F464, 0x1F465, 3: // 👤 👥 + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16711936") + + // === STRUCTURES === + // 🏠 House - Google homegardenbusiness + case 0x1F3E0, 0x1F3E1, 4: // 🏠 🏡 + return ("a-u-G", "\(googleUUID)/Google/homegardenbusiness.png", "-16711936") + // ⛺ Tent - Google campground + case 0x26FA, 0x1F3D5, 5: // ⛺ 🏕 + return ("a-u-G", "\(googleUUID)/Google/campground.png", "-256") + // 🏚️ Abandoned - Google info + case 0x1F6DA: // 🏚️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🏗️ Construction - Google caution + case 0x1F6D7: // 🏗️ + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // 🏭 Factory - Google info + case 0x1F3ED: // 🏭 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + + // === NATURE / TERRAIN === + // 🌲 Tree - Google parks + case 0x1F332: // 🌲 + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16711936") + // 🌳 Tree - Google parks + case 0x1F333: // 🌳 + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16711936") + // 🏔️ Mountain - Google cross-hairs + case 0x1F3D4: // 🏔️ + return ("a-u-G", "\(googleUUID)/Google/cross-hairs.png", "-1") + // ⛰️ Mountain - Google cross-hairs + case 0x26F0: // ⛰️ + return ("a-u-G", "\(googleUUID)/Google/cross-hairs.png", "-1") + // 💧 Water - Google water + case 0x1F4A7: // 💧 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // 🌊 Wave - Google water + case 0x1F30A: // 🌊 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // ☁️ Cloud - Google partly_cloudy + case 0x2601, 0x2602: // ☁ ☂ + return ("a-u-G", "\(googleUUID)/Google/partly_cloudy.png", "-1") + // 🌙 Moon - Google star + case 0x1F319: // 🌙 + return ("a-u-G", "\(googleUUID)/Google/star.png", "-16776961") + // ⚓ Anchor - Google marina + case 0x2693: // ⚓ + return ("a-u-G", "\(googleUUID)/Google/marina.png", "-16776961") + // ⭐ Star - Google star + case 0x2B50, 0x1F31F: // ⭐ 🌟 + return ("a-u-G", "\(googleUUID)/Google/star.png", "-256") + // 🌞 Sun - Google sunny + case 0x1F31E: // 🌞 + return ("a-u-G", "\(googleUUID)/Google/sunny.png", "-256") + + // === FLAGS/MARKERS === + // 🚩 Flag - Google flag + case 0x1F6A9: // 🚩 + return ("a-u-G", "\(googleUUID)/Google/flag.png", "-16776961") + // 🏁 Checkered flag - Google flag + case 0x1F3C1, 7: // 🏁 + return ("a-u-G", "\(googleUUID)/Google/flag.png", "-1") + // 🎌 Flags - Google flag + case 0x1F38C: // 🎌 + return ("a-u-G", "\(googleUUID)/Google/flag.png", "-16776961") + + // === OBJECTS === + // 📷 Camera - Google camera + case 0x1F4F7: // 📷 + return ("a-u-G", "\(googleUUID)/Google/camera.png", "-16711936") + // 🔒 Lock - Google info + case 0x1F512: // 🔒 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 🔑 Key - Google info + case 0x1F511: // 🔑 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 📦 Package - Google shopping + case 0x1F4E6: // 📦 + return ("a-u-G", "\(googleUUID)/Google/shopping.png", "-16711936") + // 🚧 Construction - Google caution + case 0x1F6A7: // 🚧 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-256") + // 🎯 Target - Google target + case 0x1F3AF: // 🎯 + return ("a-u-G", "\(googleUUID)/Google/target.png", "-16776961") + // 🏹 Sports bow - Google target + case 0x1F3F9: // 🏹 + return ("a-u-G", "\(googleUUID)/Google/target.png", "-16776961") + // 🔧 Wrench - Google mechanic + case 0x1F527: // 🔧 + return ("a-u-G", "\(googleUUID)/Google/mechanic.png", "-16711936") + // 🛠️ Tools - Google mechanic + case 0x1F6E0: // 🛠️ + return ("a-u-G", "\(googleUUID)/Google/mechanic.png", "-16711936") + // 📮 Post box - Google post_office + case 0x1F4EE: // 📮 + return ("a-u-G", "\(googleUUID)/Google/post_office.png", "-16776961") + // 💎 Gem - Google star + case 0x1F48E: // 💎 + return ("a-u-G", "\(googleUUID)/Google/star.png", "-16776961") + // 🔔 Bell - Google info + case 0x1F514: // 🔔 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-256") + // 🎁 Gift - Google shopping + case 0x1F381: // 🎁 + return ("a-u-G", "\(googleUUID)/Google/shopping.png", "-16776961") + // ❄️ Snowflake - Google snowflake_simple + case 0x2744: // ❄ + return ("a-u-G", "\(googleUUID)/Google/snowflake_simple.png", "-1") + // ☂️ Umbrella - Google sunny + case 0x26F1: // ⛱ + return ("a-u-G", "\(googleUUID)/Google/sunny.png", "-16776961") + // 💡 Light - Google info-i + case 0x1F4A1: // 💡 + return ("a-u-G", "\(googleUUID)/Google/info-i.png", "-256") + // 🔋 Battery - Google bars + case 0x1F50B: // 🔋 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-16711936") + // 📻 Radio - Google radio + case 0x1F4FB: // 📻 + return ("a-u-G", "\(googleUUID)/Google/radio.png", "-16711936") + // 📞 Phone - Google phone + case 0x1F4DE, 0x1F4F1: // 📞 📱 + return ("a-u-G", "\(googleUUID)/Google/phone.png", "-16711936") + // 💥 Collision - Google caution + case 0x1F4A5: // 💥 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // 🔦 Flashlight - Google sunny + case 0x1F526: // 🔦 + return ("a-u-G", "\(googleUUID)/Google/sunny.png", "-16711936") + // 🕯️ Candle - Google sunny + case 0x1F56F: // 🕯️ + return ("a-u-G", "\(googleUUID)/Google/sunny.png", "-16776961") + // 📺 TV - Google camera + case 0x1F4FA: // 📺 + return ("a-u-G", "\(googleUUID)/Google/camera.png", "-16711936") + // 💾 Disk - Google info + case 0x1F4BE: // 💾 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 📀 DVD - Google info + case 0x1F4C0: // 📀 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🖥️ Computer - Google info + case 0x1F5A5: // 🖥️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // ⌨️ Keyboard - Google info + case 0x1F5A8: // ⌨️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 🖱️ Mouse - Google info + case 0x1F5B1: // 🖱️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + + // === SYMBOLS === + // ❤️ Heart - Google flag + case 0x2764, 0x1F493, 0x1F49A, 0x1F499: // ❤️ 💓 💚 💙 + return ("a-u-G", "\(googleUUID)/Google/flag.png", "-16776961") + // ✅ Check - Google star + case 0x2705, 0x1F7E2: // ✅ 🟢 + return ("a-u-G", "\(googleUUID)/Google/star.png", "-16711936") + // ❌ X - Google caution + case 0x274C, 0x1F6AB: // ❌ 🚫 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // ➰ Curly loop - Google trail + case 0x1F0: // ➰ + return ("a-u-G", "\(googleUUID)/Google/trail.png", "-16776961") + // ➿ Double curly loop - Google trail + case 0x1F1F: // ➿ + return ("a-u-G", "\(googleUUID)/Google/trail.png", "-16776961") + + // === WEATHER === + // 🌤️ Sun behind cloud - Google partly_cloudy + case 0x1F324: // 🌤️ + return ("a-u-G", "\(googleUUID)/Google/partly_cloudy.png", "-256") + // 🌧️ Rain - Google rainy + case 0x1F327: // 🌧️ + return ("a-u-G", "\(googleUUID)/Google/rainy.png", "-16776961") + // 🌨️ Snow - Google snowflake_simple + case 0x1F328: // 🌨️ + return ("a-u-G", "\(googleUUID)/Google/snowflake_simple.png", "-1") + // 🌩️ Lightning - Google caution + case 0x1F329: // 🌩 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-256") + // 🌀 Cyclone - Google sunny + case 0x1F300: // 🌀 + return ("a-u-G", "\(googleUUID)/Google/sunny.png", "-16776961") + // 🌈 Rainbow - Google star + case 0x1F308: // 🌈 + return ("a-u-G", "\(googleUUID)/Google/star.png", "-16776961") + // 🌪️ Tornado - Google caution + case 0x1F32A: // 🌪️ + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-1") + // 🌋 Volcano - Google volcano + case 0x1F30B: // 🌋 + return ("a-u-G", "\(googleUUID)/Google/volcano.png", "-16776961") + // 🏜️ Desert - Google parks + case 0x1F3DC: // 🏜️ + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16776961") + // 🌫️ Fog - Google partly_cloudy + case 0x1F32B: // 🌫️ + return ("a-u-G", "\(googleUUID)/Google/partly_cloudy.png", "-16776961") + // 🌬️ Wind - Google partly_cloudy + case 0x1F32C: // 🌬️ + return ("a-u-G", "\(googleUUID)/Google/partly_cloudy.png", "-16711936") + + // === GLOBE === + // 🌍 Globe - Generic placemark_circle + case 0x1F30D, 0x1F30E, 0x1F30F, 0x1F310: // 🌍 🌎 🌏 🌐 + return ("a-u-G", "\(genericUUID)/Shapes/placemark_circle.png", "-16776961") + // 🗺️ Map - Generic placemark_square + case 0x1F5FA: // 🗺 + return ("a-u-G", "\(genericUUID)/Shapes/placemark_square.png", "-16776961") + // 🧭 Compass - Generic compass (use trail) + case 0x1F6AD: // 🧭 + return ("a-u-G", "\(googleUUID)/Google/trail.png", "-16776961") + + // === FOOD === + // 🍔 Burger - Google dining + case 0x1F354: // 🍔 + return ("a-u-G", "\(googleUUID)/Google/dining.png", "-256") + // 🍕 Pizza - Google dining + case 0x1F355: // 🍕 + return ("a-u-G", "\(googleUUID)/Google/dining.png", "-256") + // ☕ Coffee - Google coffee + case 0x2615: // ☕ + return ("a-u-G", "\(googleUUID)/Google/coffee.png", "-256") + // 🍺 Beer - Google bars + case 0x1F37A: // 🍺 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-256") + // 🍷 Wine - Google bars + case 0x1F377: // 🍷 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-65281") + // 🥗 Salad - Google dining + case 0x1F957: // 🥗 + return ("a-u-G", "\(googleUUID)/Google/dining.png", "-16711936") + // 🍿 Popcorn - Google movies + case 0x1F37F: // 🍿 + return ("a-u-G", "\(googleUUID)/Google/movies.png", "-16776961") + // 🍩 Donut - Google donut + case 0x1F369: // 🍩 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🍪 Cookie - Google donut + case 0x1F36A: // 🍪 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🍫 Chocolate - Google donut + case 0x1F36B: // 🍫 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🍬 Candy - Google donut + case 0x1F36C: // 🍬 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🍭 Lollipop - Google donut + case 0x1F36D: // 🍭 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🍦 Ice Cream - Google donut + case 0x1F368: // 🍦 + return ("a-u-G", "\(googleUUID)/Google/donut.png", "-16776961") + // 🥤 Cup - Google coffee + case 0x1F964: // 🥤 + return ("a-u-G", "\(googleUUID)/Google/coffee.png", "-16776961") + // 🍵 Tea - Google coffee + case 0x1F375: // 🍵 + return ("a-u-G", "\(googleUUID)/Google/coffee.png", "-16711936") + // 🥃 Whiskey - Google bars + case 0x1F943: // 🥃 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-16776961") + // 🥂 Cheers - Google bars + case 0x1F942: // 🥂 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-16776961") + // 🍾 Bottle - Google bars + case 0x1F37E: // 🍾 + return ("a-u-G", "\(googleUUID)/Google/bars.png", "-16776961") + + // === RECREATION === + // 🎣 Fishing - Google fishing + case 0x1F3A3: // 🎣 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // ⛳ Golf - Google golf + case 0x1F3CC: // ⛳ + return ("a-u-G", "\(googleUUID)/Google/golf.png", "-16711936") + // ⛷️ Ski - Google ski + case 0x1F3BF: // ⛷️ + return ("a-u-G", "\(googleUUID)/Google/ski.png", "-16711936") + // 🏊 Swimming - Google swimming + case 0x1F3CA: // 🏊 + return ("a-u-G", "\(googleUUID)/Google/swimming.png", "-16776961") + // 🏄 Surfing - Google swimming + case 0x1F3C4: // 🏄 + return ("a-u-G", "\(googleUUID)/Google/swimming.png", "-16776961") + // 🐟 Fish - Google fishing + case 0x1F41F: // 🐟 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🌾 Farm - Google parks + case 0x1F33E: // 🌾 + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16711936") + // 🐄 Farm Animal - Google parks + case 0x1F404: // 🐄 + return ("a-u-G", "\(googleUUID)/Google/parks.png", "-16711936") + // 🐕 Dog - Google hiker + case 0x1F415: // 🐕 + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16711936") + // 🐈 Cat - Google hiker + case 0x1F431: // 🐈 + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16711936") + // 🐓 Rooster - Google info + case 0x1F413: // 🐓 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦅 Eagle - Google info + case 0x1F425: // 🦅 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦋 Butterfly - Google info + case 0x1F98B: // 🦋 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐝 Bee - Google info + case 0x1F41D: // 🐝 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐞 Beetle - Google info + case 0x1F41E: // 🐞 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦀 Crab - Google fishing + case 0x1F980: // 🦀 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🦞 Lobster - Google fishing + case 0x1F99E: // 🦞 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🐚 Shell - Google fishing + case 0x1F41A: // 🐚 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🐙 Octopus - Google fishing + case 0x1F419: // 🐙 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🦑 Squid - Google fishing + case 0x1F991: // 🦑 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🦎 Lizard - Google info + case 0x1F98E: // 🦎 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐍 Snake - Google info + case 0x1F40D: // 🐍 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦖 T-Rex - Google info + case 0x1F996: // 🦖 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦕 Sauropod - Google info + case 0x1F995: // 🦕 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦈 Shark - Google fishing + case 0x1F988: // 🦈 + return ("a-u-G", "\(googleUUID)/Google/fishing.png", "-16776961") + // 🐳 Whale - Google water + case 0x1F433: // 🐳 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // 🐬 Dolphin - Google water + case 0x1F42C: // 🐬 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // 🐊 Crocodile - Google water + case 0x1F40A: // 🐊 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // 🐆 Leopard - Google info + case 0x1F406: // 🐆 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐅 Tiger - Google info + case 0x1F405: // 🐅 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐃 Buffalo - Google info + case 0x1F403: // 🐃 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐂 Ox - Google info + case 0x1F402: // 🐂 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐎 Horse - Google info + case 0x1F434: // 🐎 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐏 Ram - Google info + case 0x1F40F: // 🐏 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐑 Sheep - Google info + case 0x1F411: // 🐑 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐐 Goat - Google info + case 0x1F410: // 🐐 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦙 Llama - Google info + case 0x1F999: // 🦙 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐕‍🦺 Service Dog - Google hiker + case 0x1F9BA: // 🐕‍🦺 + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16776961") + // 🐩 Poodle - Google hiker + case 0x1F429: // 🐩 + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16776961") + // 🐈‍⬛ Black Cat - Google hiker + case 0x1F408: // 🐈‍⬛ + return ("a-u-G", "\(googleUUID)/Google/hiker.png", "-16776961") + // 🦝 Raccoon - Google info + case 0x1F99D: // 🦝 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦊 Fox - Google info + case 0x1F98A: // 🦊 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐻 Bear - Google info + case 0x1F43B: // 🐻 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐼 Panda - Google info + case 0x1F43C: // 🐼 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐨 Koala - Google info + case 0x1F428: // 🐨 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐯 Tiger - Google info + case 0x1F42F: // 🐯 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦁 Lion - Google info + case 0x1F981: // 🦁 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐮 Cow - Google info + case 0x1F42E: // 🐮 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐷 Pig - Google info + case 0x1F437: // 🐷 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐖 Pig (big) - Google info + case 0x1F416: // 🐖 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐗 Boar - Google info + case 0x1F417: // 🐗 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🐘 Elephant - Google info + case 0x1F418: // 🐘 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦏 Rhino - Google info + case 0x1F98F: // 🦏 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦛 Hippo - Google info + case 0x1F99B: // 🦛 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦒 Giraffe - Google info + case 0x1F992: // 🦒 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦬 Bison - Google info + case 0x1F9AC: // 🦬 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦣 Mammoth - Google info + case 0x1F9A3: // 🦣 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦌 Deer - Google info + case 0x1F98C: // 🦌 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🦌 Moose - Google info + case 0x1F98D: // 🦌 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + + // === INFRASTRUCTURE === + // 🚩 Checkpoint - Google flag + case 0x1F6A6: // 🚩 + return ("a-u-G", "\(googleUUID)/Google/flag.png", "-16776961") + // ⛔ No Entry - Google caution + case 0x26D4: // ⛔ + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // 🛑 Stop - Google caution + case 0x1F6D1: // 🛑 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // 🏢 Office Building - Google homegardenbusiness + case 0x1F3E2: // 🏢 + return ("a-u-G", "\(googleUUID)/Google/homegardenbusiness.png", "-16776961") + // 🏬 Bank - Google info + case 0x1F3E6: // 🏬 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🏩 Love Hotel - Google lodging + case 0x1F3E9: // 🏩 + return ("a-u-G", "\(googleUUID)/Google/lodging.png", "-16776961") + // 🛤️ Railway - Google rail + case 0x1F6E2: // 🛤️ + return ("a-u-G", "\(googleUUID)/Google/rail.png", "-16711936") + // 🛣️ Motorway - Google info + case 0x1F6E3: // 🛣️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🚎 Trolleybus - Google bus + case 0x1F68E: // 🚎 + return ("a-u-G", "\(googleUUID)/Google/bus.png", "-16776961") + // 🚈 Metro - Google rail + case 0x1F688: // 🚈 + return ("a-u-G", "\(googleUUID)/Google/rail.png", "-16711936") + // 🚊 Tram - Google tram + case 0x1F68A: // 🚊 + return ("a-u-G", "\(googleUUID)/Google/tram.png", "-16776961") + // 🚉 Station - Google rail + case 0x1F689: // 🚉 + return ("a-u-G", "\(googleUUID)/Google/rail.png", "-16776961") + // 🛃 Custom - Google info + case 0x1F6C3: // 🛃 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🛂 Passport control - Google info + case 0x1F6C2: // 🛂 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🚮 Litter - Google info + case 0x1F6AE: // 🚮 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 🚰 Water - Google water + case 0x1F6B0: // 🚰 + return ("a-u-G", "\(googleUUID)/Google/water.png", "-16776961") + // 🚱 Non-potable - Google caution + case 0x1F6B1: // 🚱 + return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") + // ♿ Wheelchair - Google wheel_chair_accessible + case 0x267F: // ♿ + return ("a-u-G", "\(googleUUID)/Google/wheel_chair_accessible.png", "-16711936") + // 🚻 Bathroom - Google info + case 0x1F6BB: // 🚻 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + // 🚹 Men's - Google info + case 0x1F6B9: // 🚹 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🚺 Women's - Google info + case 0x1F6BA: // 🚺 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🚼 Baby - Google info + case 0x1F6BC: // 🚼 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🚾 Loo - Google info + case 0x1F6BE: // 🚾 + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16776961") + // 🅿️ Parking - Google info + case 0x1F17F: // 🅿️ + return ("a-u-G", "\(googleUUID)/Google/info.png", "-16711936") + + // === Default - RED pushpin === + default: + return ("a-u-G", "\(genericUUID)/Tacks/red-pushpin.png", "-16776961") + } + } } diff --git a/Meshtastic/Helpers/TAK/TAKServerManager.swift b/Meshtastic/Helpers/TAK/TAKServerManager.swift index 182e47bb..b619af98 100644 --- a/Meshtastic/Helpers/TAK/TAKServerManager.swift +++ b/Meshtastic/Helpers/TAK/TAKServerManager.swift @@ -10,6 +10,44 @@ import Network import OSLog import Combine import SwiftUI +import CoreData +import MeshtasticProtobufs + +enum TAKServerError: LocalizedError { + case noServerCertificate + case noClientCACertificate + case tlsConfigurationFailed + case listenerFailed(String) + case clientNotFound + case notRunning + case primaryChannelInvalid(String) + + var errorDescription: String? { + switch self { + case .noServerCertificate: + return "No server certificate configured. Import a .p12 file with the server certificate and private key." + case .noClientCACertificate: + return "No client CA certificate configured. Import the CA certificate (.pem) used to sign client certificates." + case .tlsConfigurationFailed: + return "Failed to configure TLS settings." + case .listenerFailed(let reason): + return "Failed to start listener: \(reason)" + case .clientNotFound: + return "Client not found" + case .notRunning: + return "TAK Server is not running" + case .primaryChannelInvalid(let reason): + return reason + } + } +} + +struct PrimaryChannelIssue: Identifiable { + let id = UUID() + let title: String + let description: String + let canAutoFix: Bool +} /// Manages the TAK Server lifecycle, TLS connections, and client management /// Runs on MainActor for thread safety, following the AccessoryManager pattern @@ -23,6 +61,14 @@ final class TAKServerManager: ObservableObject { @Published private(set) var isRunning = false @Published private(set) var connectedClients: [TAKClientInfo] = [] @Published var lastError: String? + @Published private(set) var primaryChannelIssues: [PrimaryChannelIssue] = [] + @Published private(set) var readOnlyMode = false + + /// User toggle for read-only mode - locked to true if channel has issues + @AppStorage("takServerReadOnly") var userReadOnlyMode = false + + /// Enable Mesh to CoT converter - bridges Meshtastic packets to TAK format + @AppStorage("takServerMeshToCot") var meshToCotEnabled = false // MARK: - Configuration (persisted via AppStorage) @@ -89,6 +135,103 @@ final class TAKServerManager: ObservableObject { } } + // MARK: - Primary Channel Validation + + /// Check the primary channel for validity + /// Returns true if the primary channel is valid for TAK server operation + func checkPrimaryChannelValidity() { + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MyInfoEntity.fetchRequest() + + var issues: [PrimaryChannelIssue] = [] + var isValid = true + + do { + let myInfos = try context.fetch(fetchRequest) + guard let myInfo = myInfos.first, + let channels = myInfo.channels?.array as? [ChannelEntity], + let primaryChannel = channels.first(where: { $0.index == 0 || $0.role == 1 }) else { + issues.append(PrimaryChannelIssue( + title: "No Primary Channel", + description: "No primary channel found on device", + canAutoFix: false + )) + isValid = false + updateChannelStatus(issues: issues, isValid: isValid) + return + } + + let channelName = primaryChannel.name ?? "" + let channelPsk = primaryChannel.psk ?? Data() + let pskBase64 = channelPsk.base64EncodedString() + + if channelName.isEmpty { + issues.append(PrimaryChannelIssue( + title: "Unnamed Primary Channel", + description: "TAK Server requires a private channel. Please set up a dedicated TAK channel (name 'TAK' recommended). Tap the button below to auto-configure.", + canAutoFix: true + )) + isValid = false + } + + // Use byte length for encryption strength checks (not Base64 string length) + let pskBytes = channelPsk.count + if pskBytes == 0 { + issues.append(PrimaryChannelIssue( + title: "Public Channel Not Supported", + description: "TAK Server requires a private channel with encryption. Public channels expose your location and messages. Tap the button below to set up a private TAK channel.", + canAutoFix: true + )) + isValid = false + } else if channelPsk == Data([0x01]) { + // Default key is single byte 0x01 + issues.append(PrimaryChannelIssue( + title: "Default Encryption Key", + description: "TAK Server requires a unique private channel key. The default key is not secure. Tap the button below to set up a proper private TAK channel.", + canAutoFix: true + )) + isValid = false + } else if pskBytes < 16 { + // Less than 128-bit (16 bytes) + issues.append(PrimaryChannelIssue( + title: "Weak Encryption Key", + description: "TAK Server requires at least 128-bit encryption for your privacy. Tap the button below to set up a secure private TAK channel.", + canAutoFix: true + )) + isValid = false + } + + updateChannelStatus(issues: issues, isValid: isValid) + + } catch { + Logger.tak.error("Failed to fetch MyInfo for channel validation: \(error.localizedDescription)") + issues.append(PrimaryChannelIssue( + title: "Error Checking Channel", + description: "Could not verify primary channel settings", + canAutoFix: false + )) + updateChannelStatus(issues: issues, isValid: false) + } + } + + private func updateChannelStatus(issues: [PrimaryChannelIssue], isValid: Bool) { + primaryChannelIssues = issues + readOnlyMode = !isValid + + if !isValid { + userReadOnlyMode = true + } + + if !isValid && isRunning { + Logger.tak.warning("TAK Server running in read-only mode due to primary channel issues") + } + } + + /// Check if TAK client messages should be forwarded to mesh + var shouldForwardTAKToMesh: Bool { + return !userReadOnlyMode + } + // MARK: - Server Lifecycle /// Start the TAK server (TLS or TCP based on configuration) @@ -98,6 +241,8 @@ final class TAKServerManager: ObservableObject { return } + checkPrimaryChannelValidity() + let mode = useTLS ? "TLS" : "TCP" Logger.tak.info("Starting TAK Server on port \(self.port) (\(mode) mode)") @@ -333,6 +478,11 @@ final class TAKServerManager: ObservableObject { case .connected(let clientInfo): connectedClients.append(clientInfo) Logger.tak.info("TAK client connected: \(clientInfo.displayName)") + + // Send all mesh node positions to the newly connected client + if meshToCotEnabled { + await bridge?.broadcastAllNodesToTAK() + } case .clientInfoUpdated(let clientInfo): // Update the client info in our list @@ -382,6 +532,25 @@ final class TAKServerManager: ObservableObject { } } } + + /// Ensure bridge is initialized and ready for mesh-to-CoT broadcasting + /// Returns true if broadcasting is possible (meshToCotEnabled, server running, clients connected) + /// Call this before any mesh-to-CoT broadcast operations + func ensureBridgeReadyForMeshToCot() -> Bool { + guard meshToCotEnabled, isRunning, !connectedClients.isEmpty else { return false } + + if bridge == nil { + Logger.tak.info("Initializing bridge for mesh-to-CoT broadcast") + let accessoryManager = AccessoryManager.shared + let newBridge = TAKMeshtasticBridge( + accessoryManager: accessoryManager, + takServerManager: self + ) + newBridge.context = accessoryManager.context + bridge = newBridge + } + return true + } /// Send a CoT message to a specific client func send(_ cotMessage: CoTMessage, to clientId: UUID) async throws { @@ -400,6 +569,113 @@ final class TAKServerManager: ObservableObject { throw TAKServerError.clientNotFound } + // MARK: - Auto-fix Primary Channel + + /// Automatically fix the primary channel to TAK-compatible settings + /// Sets: Name="TAK", 256-bit AES key, preserves existing LoRa channel + /// Returns true if successful + func autoFixPrimaryChannel() async -> Bool { + let accessoryManager = AccessoryManager.shared + + guard accessoryManager.isConnected else { + Logger.tak.error("Cannot fix channel: Not connected to device") + return false + } + + Logger.tak.info("Auto-fixing primary channel for TAK compatibility") + + let context = PersistenceController.shared.container.viewContext + + guard let connectedNodeNum = accessoryManager.activeDeviceNum else { + Logger.tak.error("Cannot fix channel: No active device number") + return false + } + + guard let connectedNode = getNodeInfo(id: connectedNodeNum, context: context), + let user = connectedNode.user else { + Logger.tak.error("Cannot fix channel: No connected node or user found") + return false + } + + let fetchRequest = MyInfoEntity.fetchRequest() + + do { + let myInfos = try context.fetch(fetchRequest) + guard let myInfo = myInfos.first, + let channels = myInfo.channels?.array as? [ChannelEntity], + let primaryChannel = channels.first(where: { $0.index == 0 || $0.role == 1 }) else { + Logger.tak.error("Cannot fix channel: No primary channel found") + return false + } + + let newKey = generateChannelKey(size: 32) + guard let newPsk = Data(base64Encoded: newKey) else { + Logger.tak.error("Failed to decode generated channel key; aborting primary channel fix") + return false + } + + primaryChannel.name = "TAK" + primaryChannel.psk = newPsk + primaryChannel.role = 1 + primaryChannel.index = 0 + + if let mutableChannels = myInfo.channels?.mutableCopy() as? NSMutableOrderedSet { + if mutableChannels.contains(primaryChannel) { + mutableChannels.remove(primaryChannel) + mutableChannels.insert(primaryChannel, at: 0) + myInfo.channels = mutableChannels.copy() as? NSOrderedSet + } + } + + try context.save() + + var channel = Channel() + channel.index = 0 + channel.role = .primary + channel.settings.name = "TAK" + channel.settings.psk = newPsk + channel.settings.uplinkEnabled = primaryChannel.uplinkEnabled + channel.settings.downlinkEnabled = primaryChannel.downlinkEnabled + channel.settings.moduleSettings.positionPrecision = UInt32(primaryChannel.positionPrecision) + + try await accessoryManager.saveChannel(channel: channel, fromUser: user, toUser: user) + + Logger.tak.info("Successfully fixed primary channel: name=TAK, key=256-bit") + + // Also set LoRa modem preset to shortFast for optimal TAK performance + var loraConfig = Config.LoRaConfig() + loraConfig.modemPreset = .shortFast + loraConfig.usePreset = true + loraConfig.txEnabled = true + loraConfig.hopLimit = 3 + + // Get current LoRa config to preserve other settings + if let currentLoRa = connectedNode.loRaConfig { + loraConfig.region = Config.LoRaConfig.RegionCode(rawValue: Int(currentLoRa.regionCode)) ?? .unset + loraConfig.channelNum = UInt32(currentLoRa.channelNum) + loraConfig.txPower = Int32(currentLoRa.txPower) + loraConfig.bandwidth = UInt32(currentLoRa.bandwidth) + loraConfig.codingRate = UInt32(currentLoRa.codingRate) + loraConfig.spreadFactor = UInt32(currentLoRa.spreadFactor) + } + + do { + try await accessoryManager.saveLoRaConfig(config: loraConfig, fromUser: user, toUser: user) + Logger.tak.info("Successfully set LoRa modem preset to shortFast") + } catch { + Logger.tak.warning("Failed to set LoRa modem preset: \(error.localizedDescription)") + } + + checkPrimaryChannelValidity() + + return true + + } catch { + Logger.tak.error("Failed to fix primary channel: \(error.localizedDescription)") + return false + } + } + // MARK: - Status /// Get server status description @@ -414,31 +690,3 @@ final class TAKServerManager: ObservableObject { } } } - -// MARK: - Server Errors - -enum TAKServerError: LocalizedError { - case noServerCertificate - case noClientCACertificate - case tlsConfigurationFailed - case listenerFailed(String) - case clientNotFound - case notRunning - - var errorDescription: String? { - switch self { - case .noServerCertificate: - return "No server certificate configured. Import a .p12 file with the server certificate and private key." - case .noClientCACertificate: - return "No client CA certificate configured. Import the CA certificate (.pem) used to sign client certificates." - case .tlsConfigurationFailed: - return "Failed to configure TLS settings." - case .listenerFailed(let reason): - return "Failed to start listener: \(reason)" - case .clientNotFound: - return "Client not found" - case .notRunning: - return "TAK Server is not running" - } - } -} diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index 09d9d60d..cb8e3666 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -26,6 +26,7 @@ struct TAKServerConfig: View { ) private var channels: FetchedResults @StateObject private var takServer = TAKServerManager.shared + @Environment(\.dismiss) private var dismiss @State private var showingFileImporter = false @State private var importType: CertificateImportType = .p12 @State private var p12Password = "" @@ -35,17 +36,40 @@ struct TAKServerConfig: View { @State private var showingImportError = false @State private var showingFileExporter = false @State private var dataPackageURL: URL? + @State private var showingFixWarning = false + @State private var isFixingChannel = false + @State private var showShareChannels = false + @State private var showShareChannelsAlert = false + @State private var connectedNode: NodeInfoEntity? + @State private var isWarningExpanded = true private let certManager = TAKCertificateManager.shared var body: some View { Form { + if !takServer.primaryChannelIssues.isEmpty { + primaryChannelWarningSection + } serverStatusSection serverConfigSection certificatesSection dataPackageSection } .navigationTitle("TAK Server") + .onAppear { + takServer.checkPrimaryChannelValidity() + if let nodeNum = accessoryManager.activeDeviceNum { + connectedNode = getNodeInfo(id: nodeNum, context: context) + } + } + .alert("Fix Primary Channel?", isPresented: $showingFixWarning) { + Button("Cancel", role: .cancel) {} + Button("Fix Channel", role: .destructive) { + fixPrimaryChannel() + } + } message: { + Text("This will change your primary channel to:\n• Name: TAK\n• Encryption: New 256-bit AES key\n• LoRa preset: Short Fast (recommended for TAK)\n\nThis is required for TAK Server to work properly. Any existing channel sharing links will become invalid.") + } .fileImporter( isPresented: $showingFileImporter, allowedContentTypes: importType == .p12 ? [UTType(filenameExtension: "p12") ?? .pkcs12, .pkcs12] : [UTType(filenameExtension: "pem") ?? .plainText], @@ -75,6 +99,14 @@ struct TAKServerConfig: View { } message: { Text(importError ?? "Unknown error") } + .alert("Channel Fixed!", isPresented: $showShareChannelsAlert) { + Button("Share with TAK Buddies") { + showShareChannels = true + } + Button("Later", role: .cancel) {} + } message: { + Text("Your channel has been configured for TAK. To share the QR code: go to Settings > Share QR Code") + } .fileExporter( isPresented: $showingFileExporter, document: dataPackageURL.map { ZipDocument(url: $0) }, @@ -94,6 +126,65 @@ struct TAKServerConfig: View { } dataPackageURL = nil } + .navigationDestination(isPresented: $showShareChannels) { + if let node = connectedNode { + ShareChannels(node: node) + } + } + } + + // MARK: - Primary Channel Warning Section + + private var primaryChannelWarningSection: some View { + Section { + DisclosureGroup(isExpanded: $isWarningExpanded) { + VStack(alignment: .leading, spacing: 12) { + if takServer.readOnlyMode { + Text("Your primary channel is using the default settings (no name or default encryption key). TAK Server is running in read-only mode.") + .font(.subheadline) + .foregroundColor(.secondary) + } + + Text("You can fix this yourself by changing your primary channel:") + .font(.subheadline) + + VStack(alignment: .leading, spacing: 4) { + Label("Set a channel name", systemImage: "1.circle.fill") + Label("Use a 256-bit encryption key", systemImage: "2.circle.fill") + } + .font(.caption) + .foregroundColor(.secondary) + + Divider() + + Button { + showingFixWarning = true + } label: { + Label("Auto-Fix Channel", systemImage: "wand.and.stars") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .disabled(isFixingChannel) + + Text("Or fix it yourself in Channels settings, then return here.") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + .padding(.vertical, 8) + } label: { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("TAK Cannot Be Used on Public Channel") + .font(.headline) + } + } + } header: { + Text("Warning") + } } // MARK: - Server Status Section @@ -122,6 +213,19 @@ struct TAKServerConfig: View { .foregroundColor(.orange) } } + + if let node = connectedNode, + let role = node.user?.role, + let deviceRole = DeviceRoles(rawValue: Int(role)), + deviceRole != .tak && deviceRole != .takTracker { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("Device role is \"\(deviceRole.name)\". Consider setting to TAK or TAK Tracker for optimal operation.") + .font(.caption) + .foregroundColor(.orange) + } + } } header: { Text("Server Status") } @@ -150,6 +254,26 @@ struct TAKServerConfig: View { .foregroundColor(.secondary) } + Toggle(isOn: $takServer.userReadOnlyMode) { + VStack(alignment: .leading, spacing: 2) { + Text("Read-Only Mode") + Text("Meshtastic -> TAK works, TAK -> Meshtastic blocked") + .font(.caption) + .foregroundColor(.secondary) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(takServer.readOnlyMode) + + Toggle(isOn: $takServer.meshToCotEnabled) { + VStack(alignment: .leading, spacing: 2) { + Text("Mesh to CoT Converter") + Text("Bridge Meshtastic positions, nodes, waypoints, and messages to TAK/CoT format") + .font(.caption) + .foregroundColor(.secondary) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if !channels.isEmpty { Picker(selection: $takServer.channel) { ForEach(channels, id: \.index) { channel in @@ -387,6 +511,23 @@ struct TAKServerConfig: View { } } + private func fixPrimaryChannel() { + isFixingChannel = true + Task { + let success = await takServer.autoFixPrimaryChannel() + await MainActor.run { + isFixingChannel = false + if success { + takServer.userReadOnlyMode = false + showShareChannelsAlert = true + } else { + importError = "Failed to fix primary channel. Make sure you are connected to a device." + showingImportError = true + } + } + } + } + // MARK: - Data Package Generation private func generateAndShareDataPackage() { From f5afce2d0f5c63dbfce10de7dd593adbaac54beb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 2 Apr 2026 11:15:02 -0700 Subject: [PATCH 5/7] Update translations file, bump version --- Localizable.xcstrings | 243 ++++++++++++--------------- Meshtastic.xcodeproj/project.pbxproj | 26 ++- 2 files changed, 124 insertions(+), 145 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 30e6245e..8a7d3aeb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2,7 +2,6 @@ "sourceLanguage" : "en", "strings" : { "" : { - "shouldTranslate" : false, "localizations" : { "da" : { "stringUnit" : { @@ -10,7 +9,8 @@ "value" : "" } } - } + }, + "shouldTranslate" : false }, "\t%@" : { "localizations" : { @@ -225,95 +225,83 @@ }, "shouldTranslate" : false }, - " : %@" : { + ": %@" : { "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %@" - } - }, "es" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } } }, "shouldTranslate" : false }, - " : %d" : { + ": %d" : { "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %d" - } - }, "es" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } } }, @@ -3018,7 +3006,9 @@ } } }, - "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {}, + "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : { + + }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { "es" : { @@ -3863,7 +3853,9 @@ } } }, - "Add CA" : {}, + "Add CA" : { + + }, "Add Channel" : { "localizations" : { "da" : { @@ -11484,8 +11476,12 @@ } } }, - "Client CA Certificate" : {}, - "Client Configuration" : {}, + "Client CA Certificate" : { + + }, + "Client Configuration" : { + + }, "Client Hidden" : { "extractionState" : "stale", "localizations" : { @@ -12186,7 +12182,9 @@ } } }, - "Configuration" : {}, + "Configuration" : { + + }, "Configuration for: %@" : { "localizations" : { "da" : { @@ -14570,7 +14568,9 @@ } } }, - "Delete All" : {}, + "Delete All" : { + + }, "Delete all config, keys and BLE bonds? " : { "localizations" : { "es" : { @@ -18174,7 +18174,9 @@ } } }, - "Download TAK Server Data Package" : {}, + "Download TAK Server Data Package" : { + + }, "Drag & Drop Firmware Update" : { "localizations" : { "da" : { @@ -18961,7 +18963,9 @@ } } }, - "Enable TAK Server" : {}, + "Enable TAK Server" : { + + }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { "localizations" : { "da" : { @@ -19728,8 +19732,12 @@ } } }, - "Enter P12 Password" : {}, - "Enter the password for the PKCS#12 file" : {}, + "Enter P12 Password" : { + + }, + "Enter the password for the PKCS#12 file" : { + + }, "environment" : { "extractionState" : "stale", "localizations" : { @@ -23771,7 +23779,9 @@ } } }, - "Generate a data package (.zip) to configure TAK clients to connect to this server." : {}, + "Generate a data package (.zip) to configure TAK clients to connect to this server." : { + + }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { "es" : { @@ -27266,10 +27276,18 @@ } } }, - "Import" : {}, - "Import .pem" : {}, - "Import Custom .p12" : {}, - "Import Error" : {}, + "Import" : { + + }, + "Import .pem" : { + + }, + "Import Custom .p12" : { + + }, + "Import Error" : { + + }, "Import Route" : { "localizations" : { "da" : { @@ -32997,7 +33015,9 @@ } } }, - "mTLS" : {}, + "mTLS" : { + + }, "Multiplier" : { "localizations" : { "da" : { @@ -39159,7 +39179,9 @@ } } }, - "Port" : {}, + "Port" : { + + }, "Position" : { "localizations" : { "da" : { @@ -42816,7 +42838,9 @@ } } }, - "Reload Bundled Certificates" : {}, + "Reload Bundled Certificates" : { + + }, "Remote administration for: %@" : { "localizations" : { "da" : { @@ -43623,7 +43647,9 @@ } } }, - "Reset to Default" : {}, + "Reset to Default" : { + + }, "Restart" : { "localizations" : { "da" : { @@ -43676,7 +43702,9 @@ } } }, - "Restart Server" : {}, + "Restart Server" : { + + }, "Restart to the node you are connected to" : { "localizations" : { "da" : { @@ -46448,8 +46476,6 @@ } } }, - "Secure mTLS connection on port 8089. Both server and client certificates are required." : {}, - "Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent." : { "comment" : "A footer for the TAK Server configuration section.", "isCommentAutoGenerated" : true @@ -49143,7 +49169,9 @@ } } }, - "Server Certificate" : {}, + "Server Certificate" : { + + }, "Server Option" : { "localizations" : { "da" : { @@ -49190,7 +49218,9 @@ } } }, - "Server Status" : {}, + "Server Status" : { + + }, "Set" : { "localizations" : { "da" : { @@ -49237,6 +49267,10 @@ } } }, + "Set a channel name" : { + "comment" : "A label for a button that sets a channel name.", + "isCommentAutoGenerated" : true + }, "Set LoRa Region" : { "localizations" : { "da" : { @@ -49856,6 +49890,10 @@ } } }, + "Share with TAK Buddies" : { + "comment" : "A button that shares the QR code with TAK buddies.", + "isCommentAutoGenerated" : true + }, "Share your location in real-time and keep your group coordinated with integrated GPS features." : { "localizations" : { "de" : { @@ -52018,7 +52056,9 @@ } } }, - "Status" : {}, + "Status" : { + + }, "Stay Connected Anywhere" : { "localizations" : { "de" : { @@ -52656,9 +52696,18 @@ } } } + }, + "TAK Cannot Be Used on Public Channel" : { + "comment" : "A warning displayed when the user's primary channel is public.", + "isCommentAutoGenerated" : true + }, + "TAK Channel Index" : { + "comment" : "A label for the TAK channel index.", + "isCommentAutoGenerated" : true + }, + "TAK Server" : { }, - "TAK Server" : {}, "TAK Tracker" : { "extractionState" : "stale", "localizations" : { @@ -55989,7 +56038,9 @@ } } }, - "TLS Certificates" : {}, + "TLS Certificates" : { + + }, "TLS Enabled" : { "localizations" : { "da" : { @@ -62889,88 +62940,6 @@ } } } - }, - ": %@" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - } - }, - "shouldTranslate" : false - }, - ": %d" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - } - }, - "shouldTranslate" : false } }, "version" : "1.1" diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e3504190..ac3d99cf 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -82,7 +82,6 @@ 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; }; 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; }; 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; }; - AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; 2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; }; 300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; }; 3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; }; @@ -103,6 +102,7 @@ 8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; }; 9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; }; A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; }; + AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; }; ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */; }; B16C760DB291CFAB5335EADB /* TAKCertificateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */; }; @@ -412,7 +412,6 @@ 25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; - AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = ""; }; 3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = ""; }; 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = ""; }; @@ -434,6 +433,7 @@ 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetrics.swift; sourceTree = ""; }; 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKDataPackageGenerator.swift; sourceTree = ""; }; + AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconButton.swift; sourceTree = ""; }; ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; @@ -1988,6 +1988,9 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -2010,6 +2013,9 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2174,7 +2180,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.9; + MARKETING_VERSION = 2.7.10; OTHER_LDFLAGS = ( "-weak_framework", SwiftUI, @@ -2213,7 +2219,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.9; + MARKETING_VERSION = 2.7.10; OTHER_LDFLAGS = ( "-weak_framework", SwiftUI, @@ -2249,12 +2255,14 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.9; + MARKETING_VERSION = 2.7.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2282,12 +2290,14 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.9; + MARKETING_VERSION = 2.7.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; From 894e9382d8dfc5e8b981a931ad1441f0cf67dd7b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:02:32 -0700 Subject: [PATCH 6/7] Add missing SwiftUI #Preview blocks across 65 views (#1649) * Add SwiftUI previews for simple helper views Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Add previews for action buttons, ChannelForm, MetricsColumnDetail, and DeviceOnboarding Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Add previews for config views, log views, AppLog, Firmware, AppData, and UserConfig Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Add preview for PositionConfig Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix formatting bugs in #Preview blocks: restore missing .environmentObject/.environment modifiers and add proper tab indentation Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/7eeb7a54-7928-466f-8e39-b00d0012a09d Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Linting fixes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen --- .../CoreData/MessageEntityExtension.swift | 3 +-- Meshtastic/Helpers/MeshPackets.swift | 1 - .../Helpers/Mqtt/MqttClientProxyManager.swift | 1 - Meshtastic/Helpers/TAK/CoTXMLParser.swift | 8 ++------ Meshtastic/Helpers/TAK/TAKConnection.swift | 1 - Meshtastic/Views/Connect/Connect.swift | 9 ++++----- Meshtastic/Views/Connect/InvalidVersion.swift | 4 ++++ .../Helpers/BLESignalStrengthIndicator.swift | 8 ++++++++ Meshtastic/Views/Helpers/BatteryCompact.swift | 12 +++++++++++ Meshtastic/Views/Helpers/ChannelLock.swift | 12 +++++++++++ Meshtastic/Views/Helpers/CompassView.swift | 20 ++++--------------- Meshtastic/Views/Helpers/DateTimeText.swift | 8 ++++++++ Meshtastic/Views/Helpers/MeshtasticLogo.swift | 5 +++++ .../Helpers/Messages/MessageTemplate.swift | 12 +++++++++++ Meshtastic/Views/Helpers/PowerMetrics.swift | 12 +++++++++++ .../Views/Helpers/RXTXIndicatorView.swift | 9 +++++++++ .../Views/Helpers/RateLimitedButton.swift | 10 ++++++++++ Meshtastic/Views/Helpers/SecureInput.swift | 8 ++++++++ .../Weather/LocalWeatherConditions.swift | 4 ++++ Meshtastic/Views/Layouts/TraceRoute.swift | 7 +++++++ .../Messages/MessageContextMenuItems.swift | 2 -- .../Views/Messages/TapbackInputView.swift | 7 ++++++- .../Views/Nodes/DetectionSensorLog.swift | 13 ++++++++++++ Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 13 ++++++++++++ .../Views/Nodes/EnvironmentMetricsLog.swift | 13 ++++++++++++ .../Helpers/Actions/ClientHistoryButton.swift | 10 ++++++++++ .../Helpers/Actions/DeleteNodeButton.swift | 11 ++++++++++ .../Actions/ExchangePositionsButton.swift | 10 ++++++++++ .../Actions/ExchangeUserInfoButton.swift | 10 ++++++++++ .../Helpers/Actions/FavoriteNodeButton.swift | 13 ++++++++++++ .../Helpers/Actions/IgnoreNodeButton.swift | 9 +++++++++ .../Helpers/Actions/NavigateToButton.swift | 12 +++++++++++ .../Helpers/Actions/NodeAlertsButton.swift | 11 ++++++++++ .../Helpers/Actions/TraceRouteButton.swift | 12 +++++++++++ .../Map/MapContent/AnimatedNodePin.swift | 8 ++++++++ .../Nodes/Helpers/Map/MapSettingsForm.swift | 10 ++++++++++ .../Metrics Columns/MetricsColumnDetail.swift | 7 +++++++ .../Views/Nodes/Helpers/NodeListFilter.swift | 4 ++++ Meshtastic/Views/Nodes/PaxCounterLog.swift | 13 ++++++++++++ Meshtastic/Views/Nodes/PositionLog.swift | 13 ++++++++++++ Meshtastic/Views/Nodes/PowerMetricsLog.swift | 13 ++++++++++++ Meshtastic/Views/Nodes/TraceRouteLog.swift | 13 ++++++++++++ .../Views/Onboarding/DeviceOnboarding.swift | 5 +++++ Meshtastic/Views/Settings/About.swift | 6 ++++++ Meshtastic/Views/Settings/AppData.swift | 7 +++++++ Meshtastic/Views/Settings/AppLog.swift | 4 ++++ .../Views/Settings/Channels/ChannelForm.swift | 18 +++++++++++++++++ .../Settings/Config/BluetoothConfig.swift | 7 +++++++ .../Views/Settings/Config/ConfigHeader.swift | 10 ++++++++++ .../Views/Settings/Config/DeviceConfig.swift | 7 +++++++ .../Views/Settings/Config/DisplayConfig.swift | 7 +++++++ .../Views/Settings/Config/LoRaConfig.swift | 7 +++++++ .../Config/Module/AmbientLightingConfig.swift | 7 +++++++ .../Config/Module/CannedMessagesConfig.swift | 7 +++++++ .../Config/Module/DetectionSensorConfig.swift | 7 +++++++ .../Module/ExternalNotificationConfig.swift | 6 ++++++ .../Settings/Config/Module/MQTTConfig.swift | 7 +++++++ .../Config/Module/PaxCounterConfig.swift | 7 +++++++ .../Config/Module/RangeTestConfig.swift | 7 +++++++ .../Settings/Config/Module/RtttlConfig.swift | 7 +++++++ .../Settings/Config/Module/SerialConfig.swift | 7 +++++++ .../Config/Module/StoreForwardConfig.swift | 7 +++++++ .../Config/Module/TelemetryConfig.swift | 7 +++++++ .../Views/Settings/Config/NetworkConfig.swift | 7 +++++++ .../Settings/Config/PositionConfig.swift | 7 +++++++ .../Views/Settings/Config/PowerConfig.swift | 7 +++++++ .../Settings/Config/SaveConfigButton.swift | 5 +++++ .../Settings/Config/SecurityConfig.swift | 7 +++++++ Meshtastic/Views/Settings/Firmware.swift | 7 +++++++ Meshtastic/Views/Settings/GPSStatus.swift | 4 ++++ .../Views/Settings/Logs/AppLogFilter.swift | 4 ++++ .../Views/Settings/TAKServerConfig.swift | 2 -- .../Views/Settings/UpdateIntervalPicker.swift | 8 ++++++++ Meshtastic/Views/Settings/UserConfig.swift | 7 +++++++ MeshtasticTests/ConnectViewTests.swift | 6 +++--- MeshtasticTests/RouterTests.swift | 2 +- 76 files changed, 567 insertions(+), 41 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index d6d2c997..c9fab38a 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -59,8 +59,7 @@ extension MessageEntity { let users = try context.fetch(request) // If exactly one match is found, return its name - if users.count == 1, let name = users.first?.longName, !name.isEmpty - { + if users.count == 1, let name = users.first?.longName, !name.isEmpty { return "\(name)" } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 54e1661f..f6b8c485 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1278,4 +1278,3 @@ actor MeshPackets { } } } - diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 936f23ad..e0cd2473 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -184,4 +184,3 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { Logger.mqtt.debug("📲 [MQTT Client Proxy] pong") } } - diff --git a/Meshtastic/Helpers/TAK/CoTXMLParser.swift b/Meshtastic/Helpers/TAK/CoTXMLParser.swift index 7f9325e2..eada8793 100644 --- a/Meshtastic/Helpers/TAK/CoTXMLParser.swift +++ b/Meshtastic/Helpers/TAK/CoTXMLParser.swift @@ -71,10 +71,7 @@ final class CoTXMLParser: NSObject, XMLParserDelegate { } // MARK: - XMLParserDelegate - - func parser(_ parser: XMLParser, didStartElement elementName: String, - namespaceURI: String?, qualifiedName qName: String?, - attributes attributeDict: [String: String] = [:]) { + func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) { elementStack.append(elementName) currentElement = elementName currentText = "" @@ -138,8 +135,7 @@ final class CoTXMLParser: NSObject, XMLParserDelegate { } } - func parser(_ parser: XMLParser, didEndElement elementName: String, - namespaceURI: String?, qualifiedName qName: String?) { + func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { if elementName == "remarks" { remarksText = currentText.trimmingCharacters(in: .whitespacesAndNewlines) } diff --git a/Meshtastic/Helpers/TAK/TAKConnection.swift b/Meshtastic/Helpers/TAK/TAKConnection.swift index b4678f06..57340154 100644 --- a/Meshtastic/Helpers/TAK/TAKConnection.swift +++ b/Meshtastic/Helpers/TAK/TAKConnection.swift @@ -494,4 +494,3 @@ enum TAKConnectionError: LocalizedError { } } } - diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index b66b1b59..84e90c6e 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -565,18 +565,18 @@ struct DeviceConnectRow: View { } // Show transport type #if !targetEnvironment(macCatalyst) - HStack(alignment: .center){ + HStack(alignment: .center) { TransportIcon(transportType: device.transportType) if device.isManualConnection && (device.longName != nil || device.shortName != nil) { - VStack (alignment: .leading) { + VStack(alignment: .leading) { Text("Last seen device:") Text("\(String(describing: device))") } } }.padding(.top, 3.0) #else - //Different alignment for Mac - HStack(alignment: .firstTextBaseline){ + // Different alignment for Mac + HStack(alignment: .firstTextBaseline) { TransportIcon(transportType: device.transportType) if device.isManualConnection && (device.longName != nil || device.shortName != nil) { Text("Last seen device: \(String(describing: device))") @@ -609,4 +609,3 @@ struct DeviceConnectRow: View { } } } - diff --git a/Meshtastic/Views/Connect/InvalidVersion.swift b/Meshtastic/Views/Connect/InvalidVersion.swift index 5d475756..d6030139 100644 --- a/Meshtastic/Views/Connect/InvalidVersion.swift +++ b/Meshtastic/Views/Connect/InvalidVersion.swift @@ -62,3 +62,7 @@ struct InvalidVersion: View { } } } + +#Preview { + InvalidVersion(minimumVersion: "2.5.4", version: "2.3.0") +} diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index a68b3597..7e83940f 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -94,3 +94,11 @@ enum BLESignalStrength: Int { case normal = 1 case strong = 2 } + +#Preview { + HStack(spacing: 16) { + SignalStrengthIndicator(signalStrength: .weak) + SignalStrengthIndicator(signalStrength: .normal) + SignalStrengthIndicator(signalStrength: .strong) + } +} diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 60c1307a..aed8472b 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -111,3 +111,15 @@ struct BatteryCompact: View { } ?? "Unknown") } } + +#Preview { + VStack(spacing: 12) { + BatteryCompact(batteryLevel: 75, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: 50, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: 25, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: 10, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: 100, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: 101, font: .caption, iconFont: .caption, color: .gray) + BatteryCompact(batteryLevel: nil, font: .caption, iconFont: .caption, color: .gray) + } +} diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift index 2621311a..facc07cb 100644 --- a/Meshtastic/Views/Helpers/ChannelLock.swift +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -32,3 +32,15 @@ struct ChannelLock: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let encryptedChannel = ChannelEntity(context: context) + encryptedChannel.psk = Data([0x01, 0x02, 0x03, 0x04]) + let unencryptedChannel = ChannelEntity(context: context) + unencryptedChannel.psk = Data() + return HStack(spacing: 16) { + ChannelLock(channel: encryptedChannel) + ChannelLock(channel: unencryptedChannel) + } +} diff --git a/Meshtastic/Views/Helpers/CompassView.swift b/Meshtastic/Views/Helpers/CompassView.swift index 1e58b224..c7185acf 100644 --- a/Meshtastic/Views/Helpers/CompassView.swift +++ b/Meshtastic/Views/Helpers/CompassView.swift @@ -38,7 +38,7 @@ struct CompassView: View { } // Trigger a vibration if aligned with waypoint - private func checkAlignment(bearing: Double,heading: Double) { + private func checkAlignment(bearing: Double, heading: Double) { // Compute minimal angular difference between heading and bearing in [0, 180] let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360) let diff = min(rawDiff, 360 - rawDiff) @@ -53,7 +53,6 @@ struct CompassView: View { inAlignment = false } } - private func distanceToWaypoint() -> CLLocationDistance? { guard @@ -76,7 +75,6 @@ struct CompassView: View { return formatter.string(from: measurement) } - var body: some View { NavigationStack { VStack(spacing: 15) { @@ -88,14 +86,14 @@ struct CompassView: View { .foregroundColor(color) if let wp = waypointLocation { - HStack{ + HStack { Image(systemName: "mappin.and.ellipse") Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))") .font(.subheadline) } if let distance = distanceToWaypoint() { - HStack{ + HStack { Image(systemName: "lines.measurement.horizontal") Text("Distance: \(formatDistance(distance))") .font(.subheadline) @@ -137,7 +135,7 @@ struct CompassView: View { ) // Move waypoint marker outside compass .onChange(of: locationsHandler.heading) { _, _ in - checkAlignment(bearing: bearing,heading:locationsHandler.heading) + checkAlignment(bearing: bearing, heading:locationsHandler.heading) } } @@ -159,9 +157,7 @@ struct CompassView: View { } } - // MARK: - Waypoint Marker View - struct WaypointMarkerView: View { let bearing: Double let compassDegrees: Double @@ -177,9 +173,7 @@ struct WaypointMarkerView: View { } - // MARK: - Bearing Calculator - struct BearingCalculator { static func bearingBetween( @@ -205,9 +199,7 @@ struct BearingCalculator { } } - // MARK: - Marker Model - struct Marker: Hashable { let degrees: Double let label: String @@ -239,9 +231,7 @@ struct Marker: Hashable { } } - // MARK: - Compass Marker View - struct CompassMarkerView: View { let marker: Marker let compassDegrees: Double @@ -281,9 +271,7 @@ struct CompassMarkerView: View { } } - // MARK: - Preview - struct CompassView_Previews: PreviewProvider { static var previews: some View { CompassView( diff --git a/Meshtastic/Views/Helpers/DateTimeText.swift b/Meshtastic/Views/Helpers/DateTimeText.swift index 38e386f3..34ffcdcf 100644 --- a/Meshtastic/Views/Helpers/DateTimeText.swift +++ b/Meshtastic/Views/Helpers/DateTimeText.swift @@ -28,3 +28,11 @@ struct DateTimeText: View { } } } + +#Preview { + VStack { + DateTimeText(dateTime: Date()) + DateTimeText(dateTime: Calendar.current.date(byAdding: .day, value: -1, to: Date())) + DateTimeText(dateTime: nil) + } +} diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 6c717140..099728c9 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -51,3 +51,8 @@ struct MeshtasticLogo: View { #endif } } + +#Preview { + MeshtasticLogo() + .frame(width: 200, height: 44) +} diff --git a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift index e604b564..2173c5c5 100644 --- a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift +++ b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift @@ -36,3 +36,15 @@ struct MessageTemplate: View { } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let user = UserEntity(context: context) + user.longName = "Test User" + user.shortName = "TU" + let message = MessageEntity(context: context) + message.messagePayload = "Hello, World!" + message.messageTimestamp = Int32(Date().timeIntervalSince1970) + message.replyID = 0 + return MessageTemplate(user: user, message: message) +} diff --git a/Meshtastic/Views/Helpers/PowerMetrics.swift b/Meshtastic/Views/Helpers/PowerMetrics.swift index e85c0f6a..6009e5a6 100644 --- a/Meshtastic/Views/Helpers/PowerMetrics.swift +++ b/Meshtastic/Views/Helpers/PowerMetrics.swift @@ -94,3 +94,15 @@ struct PowerMetricCompactWidget: View { .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } + +#Preview { + let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) + Form { + LazyVGrid(columns: gridItemLayout) { + PowerMetricCompactWidget(type: .voltage, value: 3.72, title: "Channel 1 Voltage") + PowerMetricCompactWidget(type: .current, value: 125.3, title: "Channel 1 Current") + PowerMetricCompactWidget(type: .voltage, value: 5.01, title: "Channel 2 Voltage") + PowerMetricCompactWidget(type: .current, value: 42.7, title: "Channel 2 Current") + } + } +} diff --git a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift index 860b3734..f31ae179 100644 --- a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift +++ b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift @@ -119,3 +119,12 @@ struct LEDIndicator: View { } } } + +#Preview { + HStack(spacing: 12) { + LEDIndicator(flash: .constant(1), color: .green) + .frame(width: 10, height: 10) + LEDIndicator(flash: .constant(0), color: .red) + .frame(width: 10, height: 10) + } +} diff --git a/Meshtastic/Views/Helpers/RateLimitedButton.swift b/Meshtastic/Views/Helpers/RateLimitedButton.swift index 30c5d667..894bd526 100644 --- a/Meshtastic/Views/Helpers/RateLimitedButton.swift +++ b/Meshtastic/Views/Helpers/RateLimitedButton.swift @@ -45,6 +45,16 @@ public struct RateLimitedButton: View { } } +#Preview { + RateLimitedButton(key: "preview", rateLimit: 30, action: { }) { rateLimitInfo in + if let info = rateLimitInfo { + Label("\(Int(info.secondsRemaining))s", systemImage: "clock") + } else { + Label("Send", systemImage: "paperplane") + } + } +} + // To store the time an action occured (name by a key) and the time limit // Does not persist across app launches class RateLimitStorage: ObservableObject { diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index 687cc6fe..bbffe662 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -69,3 +69,11 @@ struct SecureInput: View { } } } + +#Preview { + List { + SecureInput("Password", text: .constant("s3cretP@ss"), isValid: .constant(true)) + SecureInput("Invalid Key", text: .constant("short"), isValid: .constant(false)) + SecureInput("Empty", text: .constant(""), isValid: .constant(true)) + } +} diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index def3ae6c..967c2c2b 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -118,3 +118,7 @@ func calculateDewPoint(temp: Float, relativeHumidity: Float, convertToLocale: Bo } return dewPointUnit.converted(to: format).value } + +#Preview { + LocalWeatherConditions(location: CLLocation(latitude: 47.6062, longitude: -122.3321)) +} diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift index fd4d7d92..f8f8e8d0 100644 --- a/Meshtastic/Views/Layouts/TraceRoute.swift +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -22,6 +22,13 @@ struct TraceRouteComponent: View { } } +#Preview { + TraceRouteComponent { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.title) + } +} + struct TraceRoute: Layout { var animatableData: AnimatablePair { get { diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 0d8843ef..8c5a301b 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -56,8 +56,6 @@ struct MessageContextMenuItems: View { let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) // Compute a relay display string if relayNode is present - - VStack { Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))") .foregroundColor(.gray) diff --git a/Meshtastic/Views/Messages/TapbackInputView.swift b/Meshtastic/Views/Messages/TapbackInputView.swift index 36a1e9b0..03c6597c 100644 --- a/Meshtastic/Views/Messages/TapbackInputView.swift +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -79,6 +79,12 @@ struct TapbackInputView: View { } } +#Preview { + TapbackInputView(text: .constant(""), isPresented: .constant(true)) { emoji in + print("Selected: \(emoji)") + } +} + extension UIView { var firstResponder: UIView? { guard !isFirstResponder else { return self } @@ -90,4 +96,3 @@ extension UIView { return nil } } - diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index f86b9acb..ae57cab0 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -141,3 +141,16 @@ struct DetectionSensorLog: View { ) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return DetectionSensorLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 90e8d119..09bca3e7 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -254,3 +254,16 @@ struct DeviceMetricsLog: View { ) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return DeviceMetricsLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 84148d2e..ca19ebee 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -186,3 +186,16 @@ struct EnvironmentMetricsLog: View { return lower...upper } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return EnvironmentMetricsLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift index 624ca183..020855e7 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift @@ -40,3 +40,13 @@ struct ClientHistoryButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let connectedNode = NodeInfoEntity(context: context) + connectedNode.num = 987654321 + return ClientHistoryButton(connectedNode: connectedNode, node: node) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 70ffc217..a7b096b3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -64,3 +64,14 @@ struct DeleteNodeButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let connectedNode = NodeInfoEntity(context: context) + connectedNode.num = 987654321 + let node = NodeInfoEntity(context: context) + node.num = 123456789 + return DeleteNodeButton(connectedNode: connectedNode, node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index f303f21e..c71e6b87 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -61,3 +61,13 @@ struct ExchangePositionsButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let connectedNode = NodeInfoEntity(context: context) + connectedNode.num = 987654321 + return ExchangePositionsButton(node: node, connectedNode: connectedNode) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift index 321b1532..595eec4f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift @@ -59,3 +59,13 @@ struct ExchangeUserInfoButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let connectedNode = NodeInfoEntity(context: context) + connectedNode.num = 987654321 + return ExchangeUserInfoButton(node: node, connectedNode: connectedNode) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift index 83bac1d3..d3a54864 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -79,3 +79,16 @@ struct FavoriteNodeButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return FavoriteNodeButton(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index c15c69d1..51a8801b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -51,3 +51,12 @@ struct IgnoreNodeButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + return IgnoreNodeButton(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift index 403d1b98..ec130098 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift @@ -54,3 +54,15 @@ struct NavigateToButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + user.num = 123456789 + node.user = user + return NavigateToButton(node: node) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift index 663d2900..179fb1af 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift @@ -31,3 +31,14 @@ struct NodeAlertsButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return NodeAlertsButton(context: context, node: node, user: user) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index 11a14460..62e696d4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -43,3 +43,15 @@ struct TraceRouteButton: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return TraceRouteButton(node: node) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift index 6868f499..245142c9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift @@ -51,6 +51,14 @@ struct AnimatedNodePin: View, Equatable { } } +#Preview { + VStack(spacing: 20) { + AnimatedNodePin(nodeColor: .systemBlue, shortName: "TN", hasDetectionSensorMetrics: false, isOnline: true, calculatedDelay: 0.0) + AnimatedNodePin(nodeColor: .systemGreen, shortName: "AB", hasDetectionSensorMetrics: true, isOnline: true, calculatedDelay: 0.2) + AnimatedNodePin(nodeColor: .systemRed, shortName: "XY", hasDetectionSensorMetrics: false, isOnline: false, calculatedDelay: 0.0) + } +} + struct PulsingCircle: View { let nodeColor: UIColor let calculatedDelay: Double diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index ca17c6fa..b284d49b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -228,3 +228,13 @@ struct MapSettingsForm: View { } } + +#Preview { + MapSettingsForm( + traffic: .constant(false), + pointsOfInterest: .constant(true), + mapLayer: .constant(.standard), + meshMap: .constant(true), + enabledOverlayConfigs: .constant(Set()) + ) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift index e456ce31..e2061044 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift @@ -99,3 +99,10 @@ struct MetricsColumnDetail: View { .interactiveDismissDisabled(false) } } + +#Preview { + MetricsColumnDetail( + columnList: MetricsColumnList(columns: []), + seriesList: MetricsSeriesList() + ) +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 3236f0d3..9f1c03c1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -208,3 +208,7 @@ struct NodeListFilter: View { .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } + +#Preview { + NodeListFilter(filters: NodeFilterParameters()) +} diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 482e0d69..c4a93e59 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -224,3 +224,16 @@ struct PaxCounterLog: View { ) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return PaxCounterLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index af307f20..f667f5ee 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -181,3 +181,16 @@ struct PositionLog: View { }) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return PositionLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/PowerMetricsLog.swift b/Meshtastic/Views/Nodes/PowerMetricsLog.swift index b4578a59..b43b1c47 100644 --- a/Meshtastic/Views/Nodes/PowerMetricsLog.swift +++ b/Meshtastic/Views/Nodes/PowerMetricsLog.swift @@ -297,3 +297,16 @@ struct PowerMetricsLog: View { ) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return PowerMetricsLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 3b84aec3..a4c4c91e 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -287,3 +287,16 @@ func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity] array.append(trh8) return array } + +#Preview { + let context = PersistenceController.preview.container.viewContext + let node = NodeInfoEntity(context: context) + node.num = 123456789 + let user = UserEntity(context: context) + user.longName = "Test Node" + user.shortName = "TN" + node.user = user + return TraceRouteLog(node: node) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d95d6977..f0c9f1f1 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -454,3 +454,8 @@ struct DeviceOnboarding: View { } } + +#Preview { + DeviceOnboarding() + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 98355864..9d95c472 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -67,3 +67,9 @@ struct AboutMeshtastic: View { .navigationBarTitleDisplayMode(.inline) } } + +#Preview { + NavigationView { + AboutMeshtastic() + } +} diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index aa7ac625..16034020 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -143,3 +143,10 @@ struct AppData: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return AppData() + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 08c09664..77f90440 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -271,3 +271,7 @@ extension AppLog { } extension OSLogEntry: @retroactive Identifiable { } + +#Preview { + AppLog() +} diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 3579b938..78f85344 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -250,3 +250,21 @@ struct ChannelForm: View { } } } + +#Preview { + ChannelForm( + channelIndex: .constant(0), + channelName: .constant("LongFast"), + channelKeySize: .constant(32), + channelKey: .constant("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="), + channelRole: .constant(1), + uplink: .constant(false), + downlink: .constant(false), + positionPrecision: .constant(14), + preciseLocation: .constant(false), + positionsEnabled: .constant(true), + hasChanges: .constant(false), + hasValidKey: .constant(true), + supportedVersion: .constant(true) + ) +} diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index a5955caa..b1934df3 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -147,3 +147,10 @@ struct BluetoothConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return BluetoothConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 1331235d..c8ed4f91 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -37,3 +37,13 @@ struct ConfigHeader: View { } } } + +#Preview { + ConfigHeader( + title: "Bluetooth Configuration", + config: \NodeInfoEntity.bluetoothConfig, + node: nil, + onAppear: { } + ) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6bfd711b..7d0814dc 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -345,3 +345,10 @@ struct DeviceConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return DeviceConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index ddc67d0f..a9240da0 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -235,3 +235,10 @@ struct DisplayConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return DisplayConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 17e18dc7..4129e133 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -321,3 +321,10 @@ struct LoRaConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return LoRaConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 515ca933..db2c98c2 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -134,3 +134,10 @@ struct AmbientLightingConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return AmbientLightingConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index cc0a87eb..54a9a6bd 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -355,3 +355,10 @@ struct CannedMessagesConfig: View { self.hasMessagesChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return CannedMessagesConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 02afcc3a..a56d42b1 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -261,3 +261,10 @@ struct DetectionSensorConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return DetectionSensorConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 23f4ef40..b3694ee3 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -283,3 +283,9 @@ struct ExternalNotificationConfig: View { } } +#Preview { + let context = PersistenceController.preview.container.viewContext + return ExternalNotificationConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 5336c5ef..a3417650 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -464,3 +464,10 @@ struct MQTTConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return MQTTConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index edc46ed0..90b83f73 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -123,3 +123,10 @@ struct PaxCounterConfig: View { paxcounterUpdateInterval = UpdateInterval(from: Int(node?.paxCounterConfig?.updateInterval ?? 1800)) } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return PaxCounterConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index ed65ce4e..74386285 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -143,3 +143,10 @@ struct RangeTestConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return RangeTestConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index d052bf7e..6f45103a 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -114,3 +114,10 @@ struct RtttlConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return RtttlConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index a773190d..ff070c09 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -205,3 +205,10 @@ struct SerialConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return SerialConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index bc35258d..577ef4b5 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -197,3 +197,10 @@ struct StoreForwardConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return StoreForwardConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 44a38cbd..caae19c3 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -241,3 +241,10 @@ struct TelemetryConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return TelemetryConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 92c5cc5e..044e2a2f 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -209,3 +209,10 @@ struct NetworkConfig: View { self.hasChanges = false } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return NetworkConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 3b7f65ec..68c8e9bd 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -562,3 +562,10 @@ struct PositionConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return PositionConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 2ad978b5..737bd7f7 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -226,3 +226,10 @@ private struct FloatField: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return PowerConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/SaveConfigButton.swift b/Meshtastic/Views/Settings/Config/SaveConfigButton.swift index 43fcb66a..e21f6a1b 100644 --- a/Meshtastic/Views/Settings/Config/SaveConfigButton.swift +++ b/Meshtastic/Views/Settings/Config/SaveConfigButton.swift @@ -58,3 +58,8 @@ struct SaveConfigButton: View { } } } + +#Preview { + SaveConfigButton(node: nil, hasChanges: .constant(true), onConfirmation: { }) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index c6a6a6d3..d8c2a969 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -428,3 +428,10 @@ struct SecurityConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return SecurityConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 490f7e01..3869d145 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -206,3 +206,10 @@ struct Firmware: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return Firmware(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/GPSStatus.swift b/Meshtastic/Views/Settings/GPSStatus.swift index c92a647c..f138f980 100644 --- a/Meshtastic/Views/Settings/GPSStatus.swift +++ b/Meshtastic/Views/Settings/GPSStatus.swift @@ -65,3 +65,7 @@ struct GPSStatus: View { } } } + +#Preview { + GPSStatus() +} diff --git a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift index 52927149..8b0bfbb3 100644 --- a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift +++ b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift @@ -169,3 +169,7 @@ struct AppLogFilter: View { .presentationBackgroundInteraction(.enabled(upThrough: .medium)) } } + +#Preview { + AppLogFilter(categories: .constant(Set()), levels: .constant(Set())) +} diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index cb8e3666..7e8b6502 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -424,9 +424,7 @@ struct TAKServerConfig: View { } } - // MARK: - Channel Label - @ViewBuilder private func channelLabel(_ channel: ChannelEntity) -> some View { if channel.name?.isEmpty ?? false { diff --git a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift index c8601624..46e59088 100644 --- a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift +++ b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift @@ -55,3 +55,11 @@ struct UpdateIntervalPicker: View { } } } + +#Preview { + UpdateIntervalPicker( + config: .broadcastShort, + pickerLabel: "Update Interval", + selectedInterval: .constant(UpdateInterval(from: 30)) + ) +} diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index ef0c1ffb..aab5a127 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -253,3 +253,10 @@ struct UserConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return UserConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/MeshtasticTests/ConnectViewTests.swift b/MeshtasticTests/ConnectViewTests.swift index cbbcd331..450591ad 100644 --- a/MeshtasticTests/ConnectViewTests.swift +++ b/MeshtasticTests/ConnectViewTests.swift @@ -55,7 +55,7 @@ struct DeviceTests { (-80, BLESignalStrength.normal), (-84, BLESignalStrength.normal), (-85, BLESignalStrength.weak), - (-100, BLESignalStrength.weak), + (-100, BLESignalStrength.weak) ]) func signalStrength(rssi: Int, expected: BLESignalStrength) { let device = Device( @@ -209,7 +209,7 @@ struct TransportTypeTests { @Test(arguments: [ (TransportType.ble, "BLE"), (TransportType.tcp, "TCP"), - (TransportType.serial, "Serial"), + (TransportType.serial, "Serial") ]) func rawValues(type: TransportType, expected: String) { #expect(type.rawValue == expected) @@ -307,7 +307,7 @@ struct NavigationStateTests { NavigationState.Tab.connect, NavigationState.Tab.nodes, NavigationState.Tab.map, - NavigationState.Tab.settings, + NavigationState.Tab.settings ]) func tabRawValues(tab: NavigationState.Tab) { #expect(NavigationState.Tab(rawValue: tab.rawValue) == tab) diff --git a/MeshtasticTests/RouterTests.swift b/MeshtasticTests/RouterTests.swift index 1175dc59..11af217c 100644 --- a/MeshtasticTests/RouterTests.swift +++ b/MeshtasticTests/RouterTests.swift @@ -214,7 +214,7 @@ struct RouterTests { ("debugLogs", SettingsNavigationState.debugLogs), ("appFiles", SettingsNavigationState.appFiles), ("firmwareUpdates", SettingsNavigationState.firmwareUpdates), - ("tak", SettingsNavigationState.tak), + ("tak", SettingsNavigationState.tak) ]) func routeSettingsPage(path: String, expected: SettingsNavigationState) async throws { try await assertRoute( From 04ef427ec891823a5fd8ff89bb4700c4e2490b05 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:10:45 -0700 Subject: [PATCH 7/7] Show which node created a waypoint and which last updated by (#1496) * Fixed some issues with waypoints and created a createdBy and lastUpdatedBy * Fix suggestions --------- Co-authored-by: Claude Sonnet 4.6 --- Localizable.xcstrings | 10 + Meshtastic/Helpers/MeshPackets.swift | 4 +- .../contents | 2 + .../Map/MapContent/MeshMapContent.swift | 1 + .../Nodes/Helpers/Map/WaypointForm.swift | 673 ++++++++++-------- Meshtastic/Views/Nodes/MeshMap.swift | 7 +- 6 files changed, 401 insertions(+), 296 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 8a7d3aeb..7caaa7cd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13802,6 +13802,9 @@ } } } + }, + "Created by:" : { + }, "Created: %@" : { "localizations" : { @@ -28406,6 +28409,9 @@ } } } + }, + "Last updated by:" : { + }, "Later" : { "comment" : "A button that dismisses an alert without taking any action.", @@ -31372,6 +31378,10 @@ "comment" : "A description of the read-only mode feature in TAK Server.", "isCommentAutoGenerated" : true }, + "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { + "comment" : "Privacy policy text for Meshtastic.", + "isCommentAutoGenerated" : true + }, "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { "localizations" : { "es" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f6b8c485..4c7ab01e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1175,7 +1175,7 @@ actor MeshPackets { // Fetch waypoint by waypointMessage.id, not packet.id let fetchWaypointRequest = WaypointEntity.fetchRequest() fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id)) - + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) // Fetch the node info to get the short name var nodeShortName: String = "?" @@ -1199,6 +1199,7 @@ actor MeshPackets { waypoint.longitudeI = waypointMessage.longitudeI waypoint.icon = Int64(waypointMessage.icon) waypoint.locked = Int64(waypointMessage.lockedTo) + waypoint.createdBy = Int64(packet.from) if waypointMessage.expire >= 1 { waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } else { @@ -1254,6 +1255,7 @@ actor MeshPackets { existingWaypoint.longitudeI = waypointMessage.longitudeI existingWaypoint.icon = Int64(waypointMessage.icon) existingWaypoint.locked = Int64(waypointMessage.lockedTo) + existingWaypoint.lastUpdatedBy = Int64(packet.from) if waypointMessage.expire >= 1 { existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } else { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents index a6e5465f..bc7e7d0b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents @@ -490,10 +490,12 @@ + + diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 480a5cba..39e42baa 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -177,6 +177,7 @@ struct MeshMapContent: MapContent { } } } + .annotationTitles(.automatic) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index b441bfb4..6054e4af 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -10,6 +10,7 @@ import MapKit import MeshtasticProtobufs import OSLog import SwiftUI +import CoreData struct WaypointForm: View { @@ -31,134 +32,218 @@ struct WaypointForm: View { @State private var lockedTo: Int64 = 0 @State private var selectedDetent: PresentationDetent = .medium @State private var waypointFailedAlert: Bool = false + @State private var createdByNode : NodeInfoEntity? = nil + @State private var lastUpdatedByNode : NodeInfoEntity? = nil + var body: some View { - NavigationStack { - if editMode { - Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint") - .font(.largeTitle) - Divider() - Form { - if let cl = LocationsHandler.currentLocation { - let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude )) - Section(header: Text("Coordinate") ) { + Group { + if editMode { + Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint") + .font(.largeTitle) + Divider() + Form { + if let cl = LocationsHandler.currentLocation { + let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude )) + Section(header: Text("Coordinate") ) { + HStack { + Text("Location:") + .foregroundColor(.secondary) + Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))") + .textSelection(.enabled) + .foregroundColor(.secondary) + .font(.caption) + + } + Button { + waypoint.coordinate.longitude = cl.longitude + waypoint.coordinate.latitude = cl.latitude + } label: { HStack { - Text("Location:") - .foregroundColor(.secondary) - Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))") - .textSelection(.enabled) - .foregroundColor(.secondary) - .font(.caption) - - } - Button { - waypoint.coordinate.longitude = cl.longitude - waypoint.coordinate.latitude = cl.latitude - } label: { - HStack { - Text("Use my Location") - Image(systemName: "location") - } - } - .accessibilityLabel("Set to current location") - HStack { - if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { - DistanceText(meters: distance) - .foregroundColor(Color.gray) - } + Text("Use my Location") + Image(systemName: "location") } } - } - Section(header: Text("Waypoint Options")) { + .accessibilityLabel("Set to current location") HStack { - Text("Name") - Spacer() - TextField( - "Name", - text: $name, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: name) { - var totalBytes = name.utf8.count - // Only mess with the value if it is too big - while totalBytes > 30 { - name = String(name.dropLast()) - totalBytes = name.utf8.count - } - waypoint.name = name.count > 0 ? name : "Dropped Pin" + if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { + DistanceText(meters: distance) + .foregroundColor(Color.gray) } } - HStack { - Text("Description") - Spacer() - TextField( - "Description", - text: $description, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: description) { - var totalBytes = description.utf8.count - // Only mess with the value if it is too big - while totalBytes > 100 { - description = String(description.dropLast()) - totalBytes = description.utf8.count - } - } - } - HStack { - Text("Icon") - Spacer() - TextField("Select an emoji", text: $icon) - .keyboardType(.emoji) - .font(.title) - .focused($iconIsFocused) - .onChange(of: icon) { _, value in - // If a second emoji is entered delete the first one - if value.count >= 1 { - if value.count > 1 { - let index = value.index(value.startIndex, offsetBy: 1) - icon = String(value[index]) - } - } - } - } - Toggle(isOn: $expires) { - Label("Expires", systemImage: "clock.badge.xmark") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if expires { - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) - } - Toggle(isOn: $locked) { - Label("Locked", systemImage: "lock") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } - .scrollDismissesKeyboard(.immediately) - HStack { - Button { - guard let deviceNum = accessoryManager.activeDeviceNum else { - Logger.mesh.warning("Send waypoint failed: No deviceNum") - return - } - if accessoryManager.isConnected { - /// Send a new or exiting waypoint - var newWaypoint = Waypoint() - if waypoint.id == 0 { - newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 30 { + name = String(name.dropLast()) + totalBytes = name.utf8.count } - newWaypoint.latitudeI = waypoint.latitudeI - newWaypoint.longitudeI = waypoint.longitudeI + waypoint.name = name.count > 0 ? name : "Dropped Pin" + } + } + HStack { + Text("Description") + Spacer() + TextField( + "Description", + text: $description, + axis: .vertical + ) + .foregroundColor(Color.gray) + .onChange(of: description) { + var totalBytes = description.utf8.count + // Only mess with the value if it is too big + while totalBytes > 100 { + description = String(description.dropLast()) + totalBytes = description.utf8.count + } + } + } + HStack { + Text("Icon") + Spacer() + EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") + .font(.title) + .focused($iconIsFocused) + .onChange(of: icon) { + // If it contains non-emoji characters, clear it + if !icon.onlyEmojis() { + icon = "" + return + } + + // If multiple emojis are entered or pasted, keep only the last one + if icon.count > 1 { + icon = String(icon.suffix(1)) + } + iconIsFocused = false + } + + + } + Toggle(isOn: $expires) { + Label("Expires", systemImage: "clock.badge.xmark") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if expires { + DatePicker("Expire", selection: $expire, in: Date.now...) + .datePickerStyle(.compact) + .font(.callout) + } + Toggle(isOn: $locked) { + Label("Locked", systemImage: "lock") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + } + .scrollContentBackground(.hidden) + .scrollDismissesKeyboard(.immediately) + HStack { + Button { + guard let deviceNum = accessoryManager.activeDeviceNum else { + Logger.mesh.warning("Send waypoint failed: No deviceNum") + return + } + if accessoryManager.isConnected { + /// Send a new or exiting waypoint + var newWaypoint = Waypoint() + if waypoint.id == 0 { + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" + newWaypoint.description_p = description + // Unicode scalar value for the icon emoji string + let unicodeScalers = icon.unicodeScalars + // First element as an UInt32 + let unicode = unicodeScalers[unicodeScalers.startIndex].value + newWaypoint.icon = unicode + if locked { + if lockedTo == 0 { + newWaypoint.lockedTo = UInt32(deviceNum) + } else { + newWaypoint.lockedTo = UInt32(lockedTo) + } + } + if expires { + newWaypoint.expire = UInt32(expire.timeIntervalSince1970) + } else { + newWaypoint.expire = 0 + } + + Task { + do { + try await accessoryManager.sendWaypoint(waypoint: newWaypoint) + dismiss() + } catch { + Logger.mesh.warning("Send waypoint failed: \(error)") + Task { @MainActor in + waypointFailedAlert = true + } + } + } + } else { + Logger.mesh.warning("Send waypoint failed, node not connected") + } + } label: { + Label("Send", systemImage: "arrow.up") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .disabled(!accessoryManager.isConnected) + .padding(.bottom) + + Button(role: .cancel) { + dismiss() + } label: { + Label("Cancel", systemImage: "x.circle") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(.bottom) + + if waypoint.id > 0 && accessoryManager.isConnected { + + Menu { + Button("For me", action: { + context.delete(waypoint) + do { + try context.save() + } catch { + context.rollback() + } + dismiss() }) + Button("For everyone", action: { + guard let deviceNum = accessoryManager.activeDeviceNum else { + Logger.mesh.error("Unable to set waypoint: No Device num") + return + } + var newWaypoint = Waypoint() + newWaypoint.id = UInt32(waypoint.id) newWaypoint.name = name.count > 0 ? name : "Dropped Pin" newWaypoint.description_p = description + newWaypoint.latitudeI = waypoint.latitudeI + newWaypoint.longitudeI = waypoint.longitudeI // Unicode scalar value for the icon emoji string let unicodeScalers = icon.unicodeScalars // First element as an UInt32 @@ -171,101 +256,28 @@ struct WaypointForm: View { newWaypoint.lockedTo = UInt32(lockedTo) } } - if expires { - newWaypoint.expire = UInt32(expire.timeIntervalSince1970) - } else { - newWaypoint.expire = 0 - } - + newWaypoint.expire = UInt32(1) Task { do { try await accessoryManager.sendWaypoint(waypoint: newWaypoint) - dismiss() - } catch { - Logger.mesh.warning("Send waypoint failed: \(error)") Task { @MainActor in + context.delete(waypoint) + do { + try context.save() + } catch { + context.rollback() + } + dismiss() + } + } catch { + Logger.mesh.warning("Send waypoint failed") + Task {@MainActor in waypointFailedAlert = true } } } - } else { - Logger.mesh.warning("Send waypoint failed, node not connected") - } - } label: { - Label("Send", systemImage: "arrow.up") + }) } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .disabled(!accessoryManager.isConnected) - .padding(.bottom) - - Button(role: .cancel) { - dismiss() - } label: { - Label("Cancel", systemImage: "x.circle") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(.bottom) - - if waypoint.id > 0 && accessoryManager.isConnected { - - Menu { - Button("For me", action: { - context.delete(waypoint) - do { - try context.save() - } catch { - context.rollback() - } - dismiss() }) - Button("For everyone", action: { - guard let deviceNum = accessoryManager.activeDeviceNum else { - Logger.mesh.error("Unable to set waypoint: No Device num") - return - } - var newWaypoint = Waypoint() - newWaypoint.id = UInt32(waypoint.id) - newWaypoint.name = name.count > 0 ? name : "Dropped Pin" - newWaypoint.description_p = description - newWaypoint.latitudeI = waypoint.latitudeI - newWaypoint.longitudeI = waypoint.longitudeI - // Unicode scalar value for the icon emoji string - let unicodeScalers = icon.unicodeScalars - // First element as an UInt32 - let unicode = unicodeScalers[unicodeScalers.startIndex].value - newWaypoint.icon = unicode - if locked { - if lockedTo == 0 { - newWaypoint.lockedTo = UInt32(deviceNum) - } else { - newWaypoint.lockedTo = UInt32(lockedTo) - } - } - newWaypoint.expire = UInt32(1) - Task { - do { - try await accessoryManager.sendWaypoint(waypoint: newWaypoint) - Task { @MainActor in - context.delete(waypoint) - do { - try context.save() - } catch { - context.rollback() - } - dismiss() - } - } catch { - Logger.mesh.warning("Send waypoint failed") - Task {@MainActor in - waypointFailedAlert = true - } - } - } - }) - } label: { Label("Delete", systemImage: "trash") .foregroundColor(.red) @@ -274,130 +286,167 @@ struct WaypointForm: View { .buttonBorderShape(.capsule) .controlSize(.regular) .padding(.bottom) + } + } + } else { + VStack { + HStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50) + Spacer() + Text(waypoint.name ?? "?") + .font(.largeTitle) + Spacer() + if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) { + Image(systemName: "lock.fill") + .font(.largeTitle) + } else { + Button { + editMode = true + selectedDetent = .fraction(0.85) + } label: { + Image(systemName: "square.and.pencil" ) + .font(.largeTitle) + .symbolRenderingMode(.hierarchical) + .foregroundColor(.accentColor) + } } } - } else { - VStack { - HStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50) - Spacer() - Text(waypoint.name ?? "?") - .font(.largeTitle) - Spacer() - if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) { - Image(systemName: "lock.fill") - .font(.largeTitle) - } else { - Button { - editMode = true - selectedDetent = .fraction(0.85) - } label: { - Image(systemName: "square.and.pencil" ) - .font(.largeTitle) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.accentColor) + Divider() + VStack(alignment: .leading) { + + // Nodes who created/modified + VStack(alignment: .leading, spacing: 12) { + if let created = createdByNode { + VStack(alignment: .leading, spacing: 6) { + Text("Created by:") + .font(.headline) + + HStack(spacing: 8) { + CircleText( + text: created.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(created.user?.num ?? 0x808080))) + ) + Text(created.user?.longName ?? "Unknown") + .font(.body) + } + } + } + + if let updated = lastUpdatedByNode { + VStack(alignment: .leading, spacing: 6) { + Text("Last updated by:") + .font(.headline) + + HStack(spacing: 8) { + CircleText( + text: updated.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(updated.user?.num ?? 0x808080))) + ) + Text(updated.user?.longName ?? "Unknown") + .font(.body) + } } } } - Divider() - VStack(alignment: .leading) { - // Description - if (waypoint.longDescription ?? "").count > 0 { - Label { - Text(waypoint.longDescription ?? "") - .foregroundColor(.primary) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - } icon: { - Image(systemName: "doc.plaintext") - } - .padding(.bottom) - } - /// Coordinate + .padding(.bottom) + + // Description + if (waypoint.longDescription ?? "").count > 0 { Label { - Text("Coordinates:") + Text(waypoint.longDescription ?? "") .foregroundColor(.primary) - Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) - .foregroundColor(.secondary) - .font(.caption2) } icon: { - Image(systemName: "mappin.circle") + Image(systemName: "doc.plaintext") } .padding(.bottom) - // Drop Maps Pin - Button(action: { - if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") { - UIApplication.shared.open(url) - } - }) { - Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse") + } + /// Coordinate + Label { + Text("Coordinates:") + .foregroundColor(.primary) + Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") + .textSelection(.enabled) + .foregroundColor(.secondary) + .font(.caption2) + } icon: { + Image(systemName: "mappin.circle") + } + .padding(.bottom) + // Drop Maps Pin + Button(action: { + if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") { + UIApplication.shared.open(url) } - .padding(.bottom) - /// Created + }) { + Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse") + } + .padding(.bottom) + /// Created + Label { + Text("Created: \(waypoint.created?.formatted() ?? "?")") + .foregroundColor(.primary) + } icon: { + Image(systemName: "clock.badge.checkmark") + .symbolRenderingMode(.hierarchical) + } + .padding(.bottom) + /// Updated + if waypoint.lastUpdated != nil { Label { - Text("Created: \(waypoint.created?.formatted() ?? "?")") + Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")") .foregroundColor(.primary) } icon: { - Image(systemName: "clock.badge.checkmark") + Image(systemName: "clock.arrow.circlepath") .symbolRenderingMode(.hierarchical) } .padding(.bottom) - /// Updated - if waypoint.lastUpdated != nil { - Label { - Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")") - .foregroundColor(.primary) - } icon: { - Image(systemName: "clock.arrow.circlepath") - .symbolRenderingMode(.hierarchical) - } - .padding(.bottom) + } + /// Expires + if waypoint.expire != nil { + Label { + Text("Expires: \(waypoint.expire?.formatted() ?? "?")") + .foregroundColor(.primary) + } icon: { + Image(systemName: "hourglass.bottomhalf.filled") + .symbolRenderingMode(.hierarchical) } - /// Expires - if waypoint.expire != nil { + .padding(.bottom, 5) + } + /// Distance + if let cl = LocationsHandler.currentLocation { + if cl.distance(from: cl) > 0.0 { + let metersAway = waypoint.coordinate.distance(from: cl) Label { - Text("Expires: \(waypoint.expire?.formatted() ?? "?")") + Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) } icon: { - Image(systemName: "hourglass.bottomhalf.filled") + Image(systemName: "lines.measurement.horizontal") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) } - /// Distance - if let cl = LocationsHandler.currentLocation { - if cl.distance(from: cl) > 0.0 { - let metersAway = waypoint.coordinate.distance(from: cl) - Label { - Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") - .foregroundColor(.primary) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - .padding(.bottom, 5) - } - } } - .padding(.top) -#if targetEnvironment(macCatalyst) - Spacer() - Button { - dismiss() - } label: { - Label("Close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() -#endif } + .padding(.top) +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("Close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() +#endif } } + } .alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) { Button("OK", role: .cancel) { context.delete(waypoint) @@ -421,6 +470,9 @@ struct WaypointForm: View { } } } + .task { + await fetchNodeInfo() + } .onAppear { if waypoint.id > 0 { let waypoint = getWaypoint(id: Int64(waypoint.id), context: context) @@ -453,4 +505,37 @@ struct WaypointForm: View { .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } + + private func fetchNodeInfo() async { + // --- Fetch createdBy node --- + if waypoint.createdBy != 0 { + let createdByFetch: NSFetchRequest = NodeInfoEntity.fetchRequest() + createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy)) + createdByFetch.fetchLimit = 1 + + do { + let nodes = try context.fetch(createdByFetch) + createdByNode = nodes.first + } catch { + Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)") + } + } + + // --- Fetch lastUpdatedBy node (only if different from createdBy) --- + if waypoint.lastUpdatedBy != 0, + waypoint.lastUpdatedBy != waypoint.createdBy { + let updatedByFetch: NSFetchRequest = NodeInfoEntity.fetchRequest() + updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy)) + updatedByFetch.fetchLimit = 1 + + do { + let nodes = try context.fetch(updatedByFetch) + lastUpdatedByNode = nodes.first + } catch { + Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)") + } + } + } } + + diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 3e268afa..b1bf58ba 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -120,12 +120,17 @@ struct MeshMap: View { } .sheet(item: $selectedWaypoint) { selection in WaypointForm(waypoint: selection) - .presentationDetents([.large]) + .padding() + .presentationDetents([.large]) // full screen + .presentationDragIndicator(.visible) } .sheet(item: $editingWaypoint) { selection in WaypointForm(waypoint: selection, editMode: true) + .padding() .presentationDetents([.large]) + .presentationDragIndicator(.visible) } + .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs) }