diff --git a/.gitignore b/.gitignore index 88295e7c..fa4d28d6 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json +.contextstream/ # Cloudflare Wrangler .wrangler diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 022ea399..87595dcd 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -675,6 +675,27 @@ class MeshCoreConnector extends ChangeNotifier { } } + void setContactUnreadCount(String contactKeyHex, int count) { + _contactUnreadCount[contactKeyHex] = count; + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), + ); + notifyListeners(); + } + + void setChannelUnreadCount(int channelIndex, int count) { + final channel = _findChannelByIndex(channelIndex); + if (channel != null) { + channel.unreadCount = count; + unawaited( + _channelStore.saveChannels( + _channels.isNotEmpty ? _channels : _cachedChannels, + ), + ); + notifyListeners(); + } + } + void markChannelRead(int channelIndex) { final channel = _findChannelByIndex(channelIndex); if (channel != null && channel.unreadCount > 0) { @@ -2157,6 +2178,7 @@ class MeshCoreConnector extends ChangeNotifier { return; } _bleInitialSyncStarted = true; + _pendingInitialContactsSync = true; await _requestDeviceInfo(); _startBatteryPolling(); @@ -3030,13 +3052,7 @@ class MeshCoreConnector extends ChangeNotifier { _pendingChannelSentQueue.add(message.messageId); notifyListeners(); - final trimmed = text.trim(); - final isStructuredPayload = - trimmed.startsWith('g:') || trimmed.startsWith('m:'); - final outboundText = - (isChannelSmazEnabled(channel.index) && !isStructuredPayload) - ? Smaz.encodeIfSmaller(text) - : text; + final outboundText = prepareChannelOutboundText(channel.index, text); await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); await sendFrame( buildSendChannelTextMsgFrame(channel.index, outboundText), @@ -4046,7 +4062,7 @@ class MeshCoreConnector extends ChangeNotifier { ); } else { appLogger.info( - "Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings", + "Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings", tag: 'Connector', ); return; @@ -4068,7 +4084,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } @@ -4143,7 +4159,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } @@ -4166,7 +4182,9 @@ class MeshCoreConnector extends ChangeNotifier { if (_contacts.isEmpty) return 0; var latest = 0; for (final contact in _contacts) { - final seconds = contact.lastSeen.millisecondsSinceEpoch ~/ 1000; + // prefer lastmod per spec, fallback to lastseen + final source = contact.lastModified ?? contact.lastSeen; + final seconds = source.millisecondsSinceEpoch ~/ 1000; if (seconds > latest) { latest = seconds; } @@ -4495,6 +4513,16 @@ class MeshCoreConnector extends ChangeNotifier { return text; } + String prepareChannelOutboundText(int channelIndex, String text) { + final trimmed = text.trim(); + final isStructuredPayload = + trimmed.startsWith('g:') || trimmed.startsWith('m:'); + if (!isStructuredPayload && isChannelSmazEnabled(channelIndex)) { + return Smaz.encodeIfSmaller(text); + } + return text; + } + String _channelDisplayName(int channelIndex) { for (final channel in _channels) { if (channel.index != channelIndex) continue; @@ -6090,7 +6118,7 @@ class MeshCoreConnector extends ChangeNotifier { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( contactName: contact.name, - contactType: contact.typeLabel, + contactType: contact.typeLabelRaw, contactId: contact.publicKeyHex, ); } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 396d78b3..f55e9687 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -320,7 +320,7 @@ const int maxPathSize = 64; const int pathHashSize = 1; const int maxNameSize = 32; const int maxFrameSize = 172; -const int appProtocolVersion = 3; +const int appProtocolVersion = 4; // Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE). const int maxTextPayloadBytes = 160; const int _sendTextMsgOverheadBytes = @@ -720,25 +720,19 @@ Uint8List buildUpdateContactPathFrame( final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; writer.writeUInt32LE(timestamp); - if ((lat == null || lon == null) && lastModified != null) { - // If lat/lon not provided, write zeros - writer.writeInt32LE(0); - writer.writeInt32LE(0); - } else { - // Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6 - // Latitude - final latitude = lat ?? 0.0; - writer.writeInt32LE((latitude * 1e6).round()); - - // Longitude - final longitude = lon ?? 0.0; - writer.writeInt32LE((longitude * 1e6).round()); - } - - if (lastModified != null) { - // Last modified - final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; - writer.writeUInt32LE(lastModifiedTimestamp); + // Optional [Lat x4, Lon x4][timestamp x4] tail per the doc comment above. + // Emit 8 bytes of position (zero-filled when only lastModified is provided) + // followed by an optional 4-byte timestamp. Earlier code emitted the + // position block twice, which corrupted the tail and caused the firmware + // to parse the second lat as the timestamp. See #427. + final hasLocation = lat != null && lon != null; + if (hasLocation || lastModified != null) { + writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0); + writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0); + if (lastModified != null) { + final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; + writer.writeUInt32LE(lastModifiedTimestamp); + } } return writer.toBytes(); diff --git a/lib/helpers/chat_scroll_controller.dart b/lib/helpers/chat_scroll_controller.dart index d2c73fbf..c0d19747 100644 --- a/lib/helpers/chat_scroll_controller.dart +++ b/lib/helpers/chat_scroll_controller.dart @@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController { } } + /// Jumps toward an off-screen message so that lazy ListView.builder builds + /// items near it. Only visible + cacheExtent items have real heights, so we + /// use proportion of maxScrollExtent (itself an estimate from built items' + /// avg height). Call [onJumped] on the next frame to ensureVisible/scroll + /// to the exact target. + void jumpToEstimatedOffset({ + required int unreadCount, + required int totalMessages, + required VoidCallback onJumped, + }) { + if (!hasClients || totalMessages == 0) return; + final maxExtent = position.maxScrollExtent; + final jumpOffset = maxExtent * (unreadCount / totalMessages); + if (jumpOffset > 100) { + jumpTo(jumpOffset); + } + WidgetsBinding.instance.addPostFrameCallback((_) => onJumped()); + } + void scrollToBottomIfAtBottom() { // Only scroll if jump button is NOT showing (i.e., already at bottom) if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) { diff --git a/lib/helpers/utf8_length_limiter.dart b/lib/helpers/utf8_length_limiter.dart index c6acdd29..4188ec6a 100644 --- a/lib/helpers/utf8_length_limiter.dart +++ b/lib/helpers/utf8_length_limiter.dart @@ -4,8 +4,14 @@ import 'package:flutter/services.dart'; class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { final int maxBytes; + final String Function(String)? encoder; - const Utf8LengthLimitingTextInputFormatter(this.maxBytes); + const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder}); + + int _effectiveByteLength(String text) { + final effective = encoder != null ? encoder!(text) : text; + return utf8.encode(effective).length; + } @override TextEditingValue formatEditUpdate( @@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { TextEditingValue newValue, ) { if (maxBytes <= 0) return oldValue; - final bytes = utf8.encode(newValue.text); - if (bytes.length <= maxBytes) return newValue; + if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue; final truncated = _truncateToMaxBytes(newValue.text, maxBytes); return TextEditingValue( @@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { } String _truncateToMaxBytes(String text, int limit) { + if (encoder != null) { + final runes = text.runes.toList(); + while (runes.isNotEmpty && + _effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) { + runes.removeLast(); + } + return String.fromCharCodes(runes); + } final buffer = StringBuffer(); var used = 0; for (final rune in text.runes) { diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 09e01eee..35af305d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Режим на поверителност е активиран", "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", "settings_actions": "Действия", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Изпрати Реклама", "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", "settings_advertisementSent": "Реклама изпратена", @@ -1995,13 +1997,6 @@ "contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение", "contact_teleLoc": "Местоположение на телеметрията", "contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Първоначална тежест на маршрута", "appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута", "appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути", @@ -2013,7 +2008,6 @@ "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", "path_routeWeight": "{weight}/{max}", - "settings_multiAck": "Мулти-потвърди: {value}", "settings_telemetryModeUpdated": "Режим на телеметрията е обновен", "map_showOverlaps": "Покриване на ключа на повтаряча", "map_runTraceWithReturnPath": "Върни се по същия път.", @@ -2217,5 +2211,87 @@ "repeater_cliHelpRegionListDenied": "Списва региони, които забраняват движението по пътищата при наводнения.", "repeater_cliHelpStatsPackets": "(Само за серия) Показва статистически данни на ниво пакет.", "repeater_cliHelpStatsRadio": "(Само за конкретен сериал) Показва радиостатистика.", - "repeater_cliHelpStatsCore": "(Само за серийния номер) Показва основните статистически данни за фърмуера." + "repeater_cliHelpStatsCore": "(Само за серийния номер) Показва основните статистически данни за фърмуера.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Множество потвърждения", + "map_sharedAt": "Споделено", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Избрано препятствие", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.", + "losBlockedSpotsTitle": "Ограничени места", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 02c6ea9d..ba55e3ac 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_actions": "Aktionen", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Sende Ankündigung", "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", "settings_advertisementSent": "Ankündigung gesendet", @@ -2023,13 +2025,6 @@ "contact_lastSeen": "Zuletzt gesehen", "contact_clearChat": "Chat löschen", "contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade", "appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.", "appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge", @@ -2042,7 +2037,6 @@ "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert", - "settings_multiAck": "Mehrfach-Bestätigungen: {value}", "map_showOverlaps": "Überlappungen der Repeater-Taste", "map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.", "@radioStats_noiseFloor": { @@ -2245,5 +2239,87 @@ "repeater_cliHelpRegionListDenied": "Auflistung von Regionen, die den Verkehr aufgrund von Überschwemmungen verbieten.", "repeater_cliHelpStatsPackets": "(Nur für serielle Verbindungen) Zeigt Statistiken auf Paketebene.", "repeater_cliHelpStatsRadio": "(Nur für Serien) Zeigt Radiostatistiken an.", - "repeater_cliHelpStatsCore": "(Nur für serielle Schnittstellen) Zeigt grundlegende Firmware-Statistiken." + "repeater_cliHelpStatsCore": "(Nur für serielle Schnittstellen) Zeigt grundlegende Firmware-Statistiken.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Mehrere Bestätigungen", + "map_sharedAt": "Geteilt", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsTitle": "Reservierte Plätze", + "losSelectedObstructionTitle": "Ausgewählte Behinderung", + "losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}", + "losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.", + "losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit})." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 39fd8c1b..364f7244 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -12,6 +12,7 @@ "common_delete": "Delete", "common_deleteAll": "Delete All", "common_close": "Close", + "common_done": "Done", "common_edit": "Edit", "common_add": "Add", "common_settings": "Settings", @@ -177,16 +178,11 @@ "settings_telemetryEnvironmentMode": "Telemetry Environment Mode", "settings_advertLocation": "Advert Location", "settings_advertLocationSubtitle": "Include location in advert.", - "settings_multiAck": "Multi-ACKs: {value}", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, + "settings_multiAck": "Multi-ACKs", "settings_telemetryModeUpdated": "Telemetry mode updated", "settings_actions": "Actions", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Send Advertisement", "settings_sendAdvertisementSubtitle": "Broadcast presence now", "settings_advertisementSent": "Advertisement sent", @@ -522,6 +518,14 @@ }, "channels_hashtagChannel": "Hashtag channel", "channels_public": "Public", + "channels_via": "via {path}", + "@channels_via": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "channels_private": "Private", "channels_publicChannel": "Public channel", "channels_privateChannel": "Private channel", @@ -764,6 +768,7 @@ } }, "chat_successes": "successes", + "chat_score": "Score", "chat_removePath": "Remove path", "chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.", "chat_pathActions": "Path Actions:", @@ -815,6 +820,8 @@ } } }, + "chat_markAsUnread": "Mark as Unread", + "chat_newMessages": "New messages", "chat_openLink": "Open Link?", "chat_openLinkConfirmation": "Do you want to open this link in your browser?", "chat_open": "Open", @@ -860,6 +867,12 @@ "map_from": "From", "map_source": "Source", "map_flags": "Flags", + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", "map_shareMarkerHere": "Share marker here", "map_setAsMyLocation": "Set as my location", "map_pinLabel": "Pin label", @@ -894,6 +907,7 @@ "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", + "map_sharedAt": "Shared", "map_joinRoom": "Join Room", "map_manageRepeater": "Manage Repeater", "map_tapToAdd": "Tap on nodes to add them to the path.", @@ -2017,6 +2031,46 @@ "losLegendRadioHorizon": "Radio horizon", "losLegendLosBeam": "LOS beam", "losLegendTerrain": "Terrain", + "losBlockedSpotsTitle": "Blocked spots", + "losBlockedSpotsHint": "Tap a blocked spot to highlight it on the map.", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Selected obstruction", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).", + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, "losFrequencyLabel": "Frequency", "losFrequencyInfoTooltip": "View calculation details", "losFrequencyDialogTitle": "Radio horizon calculation", @@ -2255,5 +2309,36 @@ } }, "translation_translationOptions": "Translation options", - "translation_systemLanguage": "System language" + "translation_systemLanguage": "System language", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0110610e..a0e622b0 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Modo de privacidad activado", "settings_privacyModeDisabled": "Modo de privacidad desactivado", "settings_actions": "Acciones", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Enviar Anuncio", "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", "settings_advertisementSent": "Anuncio enviado", @@ -2023,13 +2025,6 @@ "contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica", "contact_teleEnv": "Entorno de Telemetría", "contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Peso inicial de la ruta", "appSettings_maxRouteWeight": "Peso máximo permitido para la ruta", "appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas", @@ -2245,5 +2240,87 @@ "repeater_cliHelpRegionListDenied": "Enumera las regiones que prohíben el tráfico debido a las inundaciones.", "repeater_cliHelpStatsPackets": "(Solo para series) Muestra estadísticas a nivel de paquetes.", "repeater_cliHelpStatsRadio": "(Solo para transmisiones en serie) Muestra estadísticas de radio.", - "repeater_cliHelpStatsCore": "(Solo para series) Muestra estadísticas clave del firmware." + "repeater_cliHelpStatsCore": "(Solo para series) Muestra estadísticas clave del firmware.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Múltiples respuestas de confirmación", + "map_sharedAt": "Compartido", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsTitle": "Espacios ocupados", + "losBlockedSpotsHint": "Seleccione un punto bloqueado para resaltarlo en el mapa.", + "losSelectedObstructionTitle": "Obstrucción seleccionada", + "losSelectedObstructionDetails": "Bloqueado por {obstruction} a una altura de {heightUnit}, a {distanceFromA} metros de A y a {distanceFromB} metros de B ({distanceUnit}).", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a43d0995..901adf55 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Mode de confidentialité activé", "settings_privacyModeDisabled": "Mode de confidentialité désactivé", "settings_actions": "Actions", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "S'annoncer", "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", "settings_advertisementSent": "Annonce envoyée", @@ -1995,13 +1997,6 @@ "contact_lastSeen": "Dernière fois vu", "contact_clearChat": "Effacer la conversation", "contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.", "appSettings_initialRouteWeight": "Poids initial de l'itinéraire", "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet", @@ -2013,7 +2008,6 @@ "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", "path_routeWeight": "{weight}/{max}", - "settings_multiAck": "Multi-ACKs : {value}", "settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour", "map_showOverlaps": "Chevauchement de la touche répétitive", "map_runTraceWithReturnPath": "Revenir sur le même chemin.", @@ -2224,5 +2218,87 @@ "repeater_cliHelpRegionListDenied": "Liste des régions qui interdisent la circulation en cas de inondation.", "repeater_cliHelpStatsPackets": "(Uniquement pour les séries) Affiche des statistiques au niveau des paquets.", "repeater_cliHelpStatsRadio": "(Uniquement pour les séries) Affiche les statistiques de la radio.", - "repeater_cliHelpStatsCore": "(Uniquement pour les séries) Affiche les statistiques du micrologicem intégré." + "repeater_cliHelpStatsCore": "(Uniquement pour les séries) Affiche les statistiques du micrologicem intégré.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Plusieurs accusés de réception", + "map_sharedAt": "Partagé", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Obstruction sélectionnée", + "losBlockedSpotsTitle": "Places occupés", + "losBlockedSpotsHint": "Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.", + "losSelectedObstructionDetails": "Bloqué par {obstruction}, à une hauteur de {heightUnit}, à une distance de {distanceFromA} par rapport à A et à une distance de {distanceFromB} par rapport à B ({distanceUnit}).", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index ef5b7aaa..ae2b0195 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -167,6 +167,8 @@ "settings_privacyModeEnabled": "Adatvédelem mód beállítva", "settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva", "settings_actions": "Tevékenységek", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Hirdetés küldése", "settings_sendAdvertisementSubtitle": "A nyilvános megjelenés", "settings_advertisementSent": "Hirdetés elküldve", @@ -2085,13 +2087,6 @@ "radioStats_stripWaiting": "Rádió adatok begyűjtése…", "radioStats_settingsTile": "Rádió statisztikák", "radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "settings_denyAll": "Elutasítom", "settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.", "settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.", @@ -2103,7 +2098,6 @@ "settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód", "settings_advertLocation": "Reklám megjelenési hely", "settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.", - "settings_multiAck": "Többszöri visszaigazolások: {value}", "settings_telemetryModeUpdated": "A telemetriamód frissítve", "contact_info": "Kapcsolattartási információk", "contact_settings": "Kapcsolat beállítások", @@ -2255,5 +2249,87 @@ "repeater_cliHelpRegionListDenied": "Felhasznál, amelyek elutasítják a árvíz okozta forgalmat.", "repeater_cliHelpStatsPackets": "(Csak sorozat) A csomagok szintjén történő statisztikát mutat.", "repeater_cliHelpStatsRadio": "(Csak sorozat) Mutat rádióstatisztikákat.", - "repeater_cliHelpStatsCore": "(Csak soros mód) A főfirmware-adatokat mutatja." + "repeater_cliHelpStatsCore": "(Csak soros mód) A főfirmware-adatokat mutatja.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Többszörös visszaigazolások", + "map_sharedAt": "Megosztva", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Kiválasztott akadály", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.", + "losBlockedSpotsTitle": "Foglalhatatlan területek", + "losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit})." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 484c82e3..a2c33ced 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Modalità privacy abilitata", "settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_actions": "Azioni", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Invia Annuncio", "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", "settings_advertisementSent": "Annuncio inviato", @@ -1995,13 +1997,6 @@ "contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base", "contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale", "contact_teleEnv": "Ambiente di telemetria", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Peso iniziale del percorso", "appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi", "appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.", @@ -2217,5 +2212,87 @@ "repeater_cliHelpRegionListDenied": "Elenca le regioni che vietano il transito in caso di alluvioni.", "repeater_cliHelpStatsPackets": "(Solo per la visualizzazione dei dati seriali) Mostra statistiche a livello di pacchetto.", "repeater_cliHelpStatsRadio": "(Solo per serie TV) Visualizza statistiche relative alla trasmissione radiofonica.", - "repeater_cliHelpStatsCore": "(Solo per serie) Visualizza le statistiche del firmware di base." + "repeater_cliHelpStatsCore": "(Solo per serie) Visualizza le statistiche del firmware di base.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "ACK multipli", + "map_sharedAt": "Condiviso", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Ostacolo selezionato", + "losBlockedSpotsHint": "Tocca un punto bloccato sulla mappa per evidenziarlo.", + "losBlockedSpotsTitle": "Posti occupati", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 64292c42..2fa3a8b4 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -167,6 +167,8 @@ "settings_privacyModeEnabled": "プライバシーモードが有効になっています", "settings_privacyModeDisabled": "プライバシーモードは無効化されています", "settings_actions": "行動", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "広告を送信する", "settings_sendAdvertisementSubtitle": "現在、放送での活動", "settings_advertisementSent": "広告が送信されました", @@ -2085,13 +2087,6 @@ "radioStats_stripWaiting": "ラジオの統計情報を取得中…", "radioStats_settingsTile": "ラジオの統計", "radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "settings_privacy": "プライバシー設定", "settings_privacySubtitle": "共有する情報の内容を管理する。", "settings_denyAll": "すべてを否定", @@ -2103,7 +2098,6 @@ "settings_telemetryEnvironmentMode": "テレメトリ環境モード", "settings_advertLocation": "広告掲載場所", "settings_advertLocationSubtitle": "広告に場所を記載してください。", - "settings_multiAck": "複数のACK:{value}", "settings_telemetryModeUpdated": "テレメトリモードが更新されました", "contact_info": "連絡先", "contact_settings": "連絡設定", @@ -2255,5 +2249,87 @@ "repeater_cliHelpRegionListDenied": "洪水による交通を遮断している地域の一覧", "repeater_cliHelpStatsPackets": "(シリアルのみ)パケットレベルの統計情報を表示します。", "repeater_cliHelpStatsRadio": "(シリーズのみ)ラジオの統計情報を表示します。", - "repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。" + "repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "複数のACK(応答)", + "map_sharedAt": "共有済み", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsHint": "地図上で、特定された場所を強調するために、該当する場所をタップしてください。", + "losSelectedObstructionTitle": "選択された障害", + "losBlockedSpotsTitle": "利用できない場所", + "losSelectedObstructionDetails": "{obstruction} によって {heightUnit} の高さで、A地点から {distanceFromA}、B地点から {distanceFromB} ({distanceUnit}) の距離で塞がれています。", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 31d3cbf2..20280d2b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -167,6 +167,8 @@ "settings_privacyModeEnabled": "개인 정보 보호 모드 활성화", "settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화", "settings_actions": "행동", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "광고 전송", "settings_sendAdvertisementSubtitle": "방송 활동", "settings_advertisementSent": "광고 전송", @@ -2085,13 +2087,6 @@ "radioStats_stripWaiting": "라디오 통계 가져오기…", "radioStats_settingsTile": "라디오 통계", "radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "settings_privacy": "개인 정보 설정", "settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.", "settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.", @@ -2103,7 +2098,6 @@ "settings_telemetryEnvironmentMode": "텔레메트리 환경 모드", "settings_advertLocation": "광고 위치", "settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.", - "settings_multiAck": "다중 ACK: {value}", "settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료", "contact_info": "연락처", "contact_settings": "연락처 설정", @@ -2255,5 +2249,87 @@ "repeater_cliHelpRegionListDenied": "홍수 발생 시 통행 금지 지역 목록", "repeater_cliHelpStatsPackets": "(전송 속도만 표시) 패킷 수준의 통계 정보를 보여줍니다.", "repeater_cliHelpStatsRadio": "(특정 시리즈만 해당) 라디오 통계 정보를 표시합니다.", - "repeater_cliHelpStatsCore": "(시리얼 번호만 표시) 핵심 펌웨어 통계 정보를 보여줍니다." + "repeater_cliHelpStatsCore": "(시리얼 번호만 표시) 핵심 펌웨어 통계 정보를 보여줍니다.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "다중 ACK", + "map_sharedAt": "공유됨", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsHint": "지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.", + "losBlockedSpotsTitle": "차단된 공간", + "losSelectedObstructionTitle": "선택된 장애물", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 916f55c3..25983110 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -202,6 +202,12 @@ abstract class AppLocalizations { /// **'Close'** String get common_close; + /// No description provided for @common_done. + /// + /// In en, this message translates to: + /// **'Done'** + String get common_done; + /// No description provided for @common_edit. /// /// In en, this message translates to: @@ -901,8 +907,8 @@ abstract class AppLocalizations { /// No description provided for @settings_multiAck. /// /// In en, this message translates to: - /// **'Multi-ACKs: {value}'** - String settings_multiAck(String value); + /// **'Multi-ACKs'** + String get settings_multiAck; /// No description provided for @settings_telemetryModeUpdated. /// @@ -916,6 +922,18 @@ abstract class AppLocalizations { /// **'Actions'** String get settings_actions; + /// No description provided for @settings_deleteAllPaths. + /// + /// In en, this message translates to: + /// **'Delete All Paths'** + String get settings_deleteAllPaths; + + /// No description provided for @settings_deleteAllPathsSubtitle. + /// + /// In en, this message translates to: + /// **'Clear all path data from contacts.'** + String get settings_deleteAllPathsSubtitle; + /// No description provided for @settings_sendAdvertisement. /// /// In en, this message translates to: @@ -2044,6 +2062,12 @@ abstract class AppLocalizations { /// **'Public'** String get channels_public; + /// No description provided for @channels_via. + /// + /// In en, this message translates to: + /// **'via {path}'** + String channels_via(String path); + /// No description provided for @channels_private. /// /// In en, this message translates to: @@ -2662,6 +2686,12 @@ abstract class AppLocalizations { /// **'successes'** String get chat_successes; + /// No description provided for @chat_score. + /// + /// In en, this message translates to: + /// **'Score'** + String get chat_score; + /// No description provided for @chat_removePath. /// /// In en, this message translates to: @@ -2824,6 +2854,18 @@ abstract class AppLocalizations { /// **'Unread: {count}'** String chat_unread(int count); + /// No description provided for @chat_markAsUnread. + /// + /// In en, this message translates to: + /// **'Mark as Unread'** + String get chat_markAsUnread; + + /// No description provided for @chat_newMessages. + /// + /// In en, this message translates to: + /// **'New messages'** + String get chat_newMessages; + /// No description provided for @chat_openLink. /// /// In en, this message translates to: @@ -2968,6 +3010,42 @@ abstract class AppLocalizations { /// **'Flags'** String get map_flags; + /// No description provided for @map_type. + /// + /// In en, this message translates to: + /// **'Type'** + String get map_type; + + /// No description provided for @map_path. + /// + /// In en, this message translates to: + /// **'Path'** + String get map_path; + + /// No description provided for @map_location. + /// + /// In en, this message translates to: + /// **'Location'** + String get map_location; + + /// No description provided for @map_estLocation. + /// + /// In en, this message translates to: + /// **'Est. Location'** + String get map_estLocation; + + /// No description provided for @map_publicKey. + /// + /// In en, this message translates to: + /// **'Public Key'** + String get map_publicKey; + + /// No description provided for @map_publicKeyPrefixHint. + /// + /// In en, this message translates to: + /// **'e.g. ab12'** + String get map_publicKeyPrefixHint; + /// No description provided for @map_shareMarkerHere. /// /// In en, this message translates to: @@ -3130,6 +3208,12 @@ abstract class AppLocalizations { /// **'Shared pin'** String get map_sharedPin; + /// No description provided for @map_sharedAt. + /// + /// In en, this message translates to: + /// **'Shared'** + String get map_sharedAt; + /// No description provided for @map_joinRoom. /// /// In en, this message translates to: @@ -6318,6 +6402,47 @@ abstract class AppLocalizations { /// **'Terrain'** String get losLegendTerrain; + /// No description provided for @losBlockedSpotsTitle. + /// + /// In en, this message translates to: + /// **'Blocked spots'** + String get losBlockedSpotsTitle; + + /// No description provided for @losBlockedSpotsHint. + /// + /// In en, this message translates to: + /// **'Tap a blocked spot to highlight it on the map.'** + String get losBlockedSpotsHint; + + /// No description provided for @losBlockedSpotChip. + /// + /// In en, this message translates to: + /// **'{distance} {distanceUnit} • {obstruction} {heightUnit}'** + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ); + + /// No description provided for @losSelectedObstructionTitle. + /// + /// In en, this message translates to: + /// **'Selected obstruction'** + String get losSelectedObstructionTitle; + + /// No description provided for @losSelectedObstructionDetails. + /// + /// In en, this message translates to: + /// **'Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).'** + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ); + /// No description provided for @losFrequencyLabel. /// /// In en, this message translates to: @@ -7066,6 +7191,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'System language'** String get translation_systemLanguage; + + /// No description provided for @background_serviceTitle. + /// + /// In en, this message translates to: + /// **'MeshCore running'** + String get background_serviceTitle; + + /// No description provided for @background_serviceText. + /// + /// In en, this message translates to: + /// **'Keeping BLE connected'** + String get background_serviceText; + + /// No description provided for @appSettings_translationModelDeleted. + /// + /// In en, this message translates to: + /// **'Deleted {name}'** + String appSettings_translationModelDeleted(String name); + + /// No description provided for @appSettings_translationModelDeleteFailed. + /// + /// In en, this message translates to: + /// **'Failed to delete: {error}'** + String appSettings_translationModelDeleteFailed(String error); + + /// No description provided for @channels_channelUpdateFailed. + /// + /// In en, this message translates to: + /// **'Failed to update channel: {error}'** + String channels_channelUpdateFailed(String error); + + /// No description provided for @contact_typeChat. + /// + /// In en, this message translates to: + /// **'Chat'** + String get contact_typeChat; + + /// No description provided for @contact_typeRepeater. + /// + /// In en, this message translates to: + /// **'Repeater'** + String get contact_typeRepeater; + + /// No description provided for @contact_typeRoom. + /// + /// In en, this message translates to: + /// **'Room'** + String get contact_typeRoom; + + /// No description provided for @contact_typeSensor. + /// + /// In en, this message translates to: + /// **'Sensor'** + String get contact_typeSensor; + + /// No description provided for @contact_typeUnknown. + /// + /// In en, this message translates to: + /// **'Unknown'** + String get contact_typeUnknown; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 0d4d25b9..2291babe 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -44,6 +44,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get common_close => 'Затвори'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Редактирай'; @@ -437,9 +440,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Включи местоположение в обявата'; @override - String settings_multiAck(String value) { - return 'Мулти-потвърди: $value'; - } + String get settings_multiAck => 'Множество потвърждения'; @override String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен'; @@ -447,6 +448,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_actions => 'Действия'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Изпрати Реклама'; @@ -1097,6 +1105,11 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_public => 'Публично'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Личен'; @@ -1458,6 +1471,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_successes => 'Успехи'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Премахни пътя'; @@ -1560,6 +1576,12 @@ class AppLocalizationsBg extends AppLocalizations { return 'Непрочетени: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Отваряне на връзката?'; @@ -1641,6 +1663,24 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_flags => 'Флаг'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Споделете маркер тук'; @@ -1726,6 +1766,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_sharedPin => 'Споделено копие'; + @override + String get map_sharedAt => 'Споделено'; + @override String get map_joinRoom => 'Присъедини се към стаята'; @@ -3663,6 +3706,37 @@ class AppLocalizationsBg extends AppLocalizations { @override String get losLegendTerrain => 'Терен'; + @override + String get losBlockedSpotsTitle => 'Ограничени места'; + + @override + String get losBlockedSpotsHint => + 'Кликнете върху блокираната точка, за да я отбележите на картата.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Избрано препятствие'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Честота'; @@ -4127,4 +4201,40 @@ class AppLocalizationsBg extends AppLocalizations { @override String get translation_systemLanguage => 'Език на системата'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 38104a7f..0be84496 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -44,6 +44,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get common_close => 'Schließen'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Bearbeiten'; @@ -435,9 +438,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Ort in der Anzeige einbeziehen'; @override - String settings_multiAck(String value) { - return 'Mehrfach-Bestätigungen: $value'; - } + String get settings_multiAck => 'Mehrere Bestätigungen'; @override String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert'; @@ -445,6 +446,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_actions => 'Aktionen'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Sende Ankündigung'; @@ -1092,6 +1100,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_public => 'Öffentlich'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privat'; @@ -1457,6 +1470,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_successes => 'Erfolgreich'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Pfad entfernen'; @@ -1557,6 +1573,12 @@ class AppLocalizationsDe extends AppLocalizations { return 'Ungelesen: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Link öffnen?'; @@ -1638,6 +1660,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_flags => 'Flaggen'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Teilen Sie den Marker hier.'; @@ -1723,6 +1763,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_sharedPin => 'Gemeinsames Passwort'; + @override + String get map_sharedAt => 'Geteilt'; + @override String get map_joinRoom => 'Beitreten Sie dem Raum'; @@ -3674,6 +3717,37 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losLegendTerrain => 'Gelände'; + @override + String get losBlockedSpotsTitle => 'Reservierte Plätze'; + + @override + String get losBlockedSpotsHint => + 'Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance • $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Ausgewählte Behinderung'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blockiert durch $obstruction in einer Höhe von $heightUnit, $distanceFromA von A und $distanceFromB von B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frequenz'; @@ -4144,4 +4218,40 @@ class AppLocalizationsDe extends AppLocalizations { @override String get translation_systemLanguage => 'Sprache des Systems'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3364e1f3..e0db1273 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get common_close => 'Close'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Edit'; @@ -427,9 +430,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_advertLocationSubtitle => 'Include location in advert.'; @override - String settings_multiAck(String value) { - return 'Multi-ACKs: $value'; - } + String get settings_multiAck => 'Multi-ACKs'; @override String get settings_telemetryModeUpdated => 'Telemetry mode updated'; @@ -437,6 +438,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_actions => 'Actions'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Send Advertisement'; @@ -1074,6 +1082,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channels_public => 'Public'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Private'; @@ -1430,6 +1443,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_successes => 'successes'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Remove path'; @@ -1527,6 +1543,12 @@ class AppLocalizationsEn extends AppLocalizations { return 'Unread: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Open Link?'; @@ -1608,6 +1630,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_flags => 'Flags'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Share marker here'; @@ -1692,6 +1732,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_sharedPin => 'Shared pin'; + @override + String get map_sharedAt => 'Shared'; + @override String get map_joinRoom => 'Join Room'; @@ -3601,6 +3644,37 @@ class AppLocalizationsEn extends AppLocalizations { @override String get losLegendTerrain => 'Terrain'; + @override + String get losBlockedSpotsTitle => 'Blocked spots'; + + @override + String get losBlockedSpotsHint => + 'Tap a blocked spot to highlight it on the map.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Selected obstruction'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frequency'; @@ -4052,4 +4126,40 @@ class AppLocalizationsEn extends AppLocalizations { @override String get translation_systemLanguage => 'System language'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9229140f..5efe2c44 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -44,6 +44,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get common_close => 'Cerrar'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Editar'; @@ -434,9 +437,7 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio'; @override - String settings_multiAck(String value) { - return 'Múltiples respuestas de confirmación: $value'; - } + String get settings_multiAck => 'Múltiples respuestas de confirmación'; @override String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado'; @@ -444,6 +445,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_actions => 'Acciones'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Enviar Anuncio'; @@ -1094,6 +1102,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channels_public => 'Público'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privado'; @@ -1455,6 +1468,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_successes => 'Éxitos'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Eliminar ruta'; @@ -1556,6 +1572,12 @@ class AppLocalizationsEs extends AppLocalizations { return 'Sin leer: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => '¿Abrir enlace?'; @@ -1637,6 +1659,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_flags => 'Banderas'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Compartir marcador aquí'; @@ -1722,6 +1762,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_sharedPin => 'Pin compartido'; + @override + String get map_sharedAt => 'Compartido'; + @override String get map_joinRoom => 'Únete a la sala'; @@ -3663,6 +3706,37 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losLegendTerrain => 'Terreno'; + @override + String get losBlockedSpotsTitle => 'Espacios ocupados'; + + @override + String get losBlockedSpotsHint => + 'Seleccione un punto bloqueado para resaltarlo en el mapa.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Obstrucción seleccionada'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Bloqueado por $obstruction a una altura de $heightUnit, a $distanceFromA metros de A y a $distanceFromB metros de B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frecuencia'; @@ -4132,4 +4206,40 @@ class AppLocalizationsEs extends AppLocalizations { @override String get translation_systemLanguage => 'Idioma del sistema'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index aab4ea33..3056f218 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -44,6 +44,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get common_close => 'Fermer'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Modifier'; @@ -438,9 +441,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Inclure l\'emplacement dans l\'annonce'; @override - String settings_multiAck(String value) { - return 'Multi-ACKs : $value'; - } + String get settings_multiAck => 'Plusieurs accusés de réception'; @override String get settings_telemetryModeUpdated => @@ -449,6 +450,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_actions => 'Actions'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'S\'annoncer'; @@ -1099,6 +1107,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_public => 'Public'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privé'; @@ -1462,6 +1475,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_successes => 'Succès'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Supprimer le chemin'; @@ -1565,6 +1581,12 @@ class AppLocalizationsFr extends AppLocalizations { return 'Non lu : $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Ouvrir le lien ?'; @@ -1647,6 +1669,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_flags => 'Drapeaux'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Partager le marqueur ici'; @@ -1732,6 +1772,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_sharedPin => 'Clé partagée'; + @override + String get map_sharedAt => 'Partagé'; + @override String get map_joinRoom => 'Rejoindre le room server'; @@ -3686,6 +3729,37 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losLegendTerrain => 'Terrain'; + @override + String get losBlockedSpotsTitle => 'Places occupés'; + + @override + String get losBlockedSpotsHint => + 'Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Obstruction sélectionnée'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Bloqué par $obstruction, à une hauteur de $heightUnit, à une distance de $distanceFromA par rapport à A et à une distance de $distanceFromB par rapport à B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Fréquence'; @@ -4161,4 +4235,40 @@ class AppLocalizationsFr extends AppLocalizations { @override String get translation_systemLanguage => 'Langue du système'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index c59817a6..0434845e 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -44,6 +44,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get common_close => 'Bezárás'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Szerkesztés'; @@ -437,9 +440,7 @@ class AppLocalizationsHu extends AppLocalizations { 'A hirdetés tartalmazza a helyszínt.'; @override - String settings_multiAck(String value) { - return 'Többszöri visszaigazolások: $value'; - } + String get settings_multiAck => 'Többszörös visszaigazolások'; @override String get settings_telemetryModeUpdated => 'A telemetriamód frissítve'; @@ -447,6 +448,13 @@ class AppLocalizationsHu extends AppLocalizations { @override String get settings_actions => 'Tevékenységek'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Hirdetés küldése'; @@ -1099,6 +1107,11 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channels_public => 'A nyilvánosság számára'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Személyes'; @@ -1467,6 +1480,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_successes => 'sikerek'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Törölje a elérési útvonalat'; @@ -1567,6 +1583,12 @@ class AppLocalizationsHu extends AppLocalizations { return 'Olvasatlan: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Nyisd meg a linket?'; @@ -1649,6 +1671,24 @@ class AppLocalizationsHu extends AppLocalizations { @override String get map_flags => 'Zászló'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt'; @@ -1735,6 +1775,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get map_sharedPin => 'Gemeinsames PIN-kód'; + @override + String get map_sharedAt => 'Megosztva'; + @override String get map_joinRoom => 'Csatlakozás a szobához'; @@ -3679,6 +3722,37 @@ class AppLocalizationsHu extends AppLocalizations { @override String get losLegendTerrain => 'Terület'; + @override + String get losBlockedSpotsTitle => 'Foglalhatatlan területek'; + + @override + String get losBlockedSpotsHint => + 'A blokkolt területet megjelölve, hogy a térképen kiemeljük.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Kiválasztott akadály'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Hatósság'; @@ -4150,4 +4224,40 @@ class AppLocalizationsHu extends AppLocalizations { @override String get translation_systemLanguage => 'Rendszer nyelvé'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 7acf2e51..b72441a3 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -44,6 +44,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get common_close => 'Chiudi'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Modifica'; @@ -437,9 +440,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Includi la posizione nell\'annuncio'; @override - String settings_multiAck(String value) { - return 'ACK multipli: $value'; - } + String get settings_multiAck => 'ACK multipli'; @override String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata'; @@ -447,6 +448,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_actions => 'Azioni'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Invia Annuncio'; @@ -1095,6 +1103,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_public => 'Pubblico'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privato'; @@ -1456,6 +1469,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_successes => 'successi'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Rimuovi percorso'; @@ -1558,6 +1574,12 @@ class AppLocalizationsIt extends AppLocalizations { return 'Non letti: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Aprire il link?'; @@ -1639,6 +1661,24 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_flags => 'Bandiere'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Condividi marcatore qui'; @@ -1723,6 +1763,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_sharedPin => 'Condividi PIN'; + @override + String get map_sharedAt => 'Condiviso'; + @override String get map_joinRoom => 'Unisciti alla stanza'; @@ -3667,6 +3710,37 @@ class AppLocalizationsIt extends AppLocalizations { @override String get losLegendTerrain => 'Terreno'; + @override + String get losBlockedSpotsTitle => 'Posti occupati'; + + @override + String get losBlockedSpotsHint => + 'Tocca un punto bloccato sulla mappa per evidenziarlo.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Ostacolo selezionato'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frequenza'; @@ -4136,4 +4210,40 @@ class AppLocalizationsIt extends AppLocalizations { @override String get translation_systemLanguage => 'Lingua del sistema'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 9f10989c..9439f399 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -44,6 +44,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get common_close => '閉じる'; + @override + String get common_done => 'Done'; + @override String get common_edit => '編集'; @@ -414,9 +417,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_advertLocationSubtitle => '広告に場所を記載してください。'; @override - String settings_multiAck(String value) { - return '複数のACK:$value'; - } + String get settings_multiAck => '複数のACK(応答)'; @override String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました'; @@ -424,6 +425,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get settings_actions => '行動'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => '広告を送信する'; @@ -1041,6 +1049,11 @@ class AppLocalizationsJa extends AppLocalizations { @override String get channels_public => '一般の人々'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => '個人の'; @@ -1394,6 +1407,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_successes => '成功事例'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'パスを削除する'; @@ -1489,6 +1505,12 @@ class AppLocalizationsJa extends AppLocalizations { return '未読: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'リンクを開く?'; @@ -1567,6 +1589,24 @@ class AppLocalizationsJa extends AppLocalizations { @override String get map_flags => '旗'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'この場所でシェア'; @@ -1650,6 +1690,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get map_sharedPin => '共有パスワード'; + @override + String get map_sharedAt => '共有済み'; + @override String get map_joinRoom => '部屋に参加する'; @@ -3471,6 +3514,36 @@ class AppLocalizationsJa extends AppLocalizations { @override String get losLegendTerrain => '地形'; + @override + String get losBlockedSpotsTitle => '利用できない場所'; + + @override + String get losBlockedSpotsHint => '地図上で、特定された場所を強調するために、該当する場所をタップしてください。'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => '選択された障害'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return '$obstruction によって $heightUnit の高さで、A地点から $distanceFromA、B地点から $distanceFromB ($distanceUnit) の距離で塞がれています。'; + } + @override String get losFrequencyLabel => '周波数'; @@ -3907,4 +3980,40 @@ class AppLocalizationsJa extends AppLocalizations { @override String get translation_systemLanguage => 'システム言語'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 5326c6bc..ab9439aa 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -44,6 +44,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get common_close => '닫기'; + @override + String get common_done => 'Done'; + @override String get common_edit => '수정'; @@ -414,9 +417,7 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.'; @override - String settings_multiAck(String value) { - return '다중 ACK: $value'; - } + String get settings_multiAck => '다중 ACK'; @override String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료'; @@ -424,6 +425,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get settings_actions => '행동'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => '광고 전송'; @@ -1036,6 +1044,11 @@ class AppLocalizationsKo extends AppLocalizations { @override String get channels_public => '대중의'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => '사립'; @@ -1390,6 +1403,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_successes => '성공 사례'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => '경로 제거'; @@ -1485,6 +1501,12 @@ class AppLocalizationsKo extends AppLocalizations { return '읽지 않음: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => '링크를 열기?'; @@ -1563,6 +1585,24 @@ class AppLocalizationsKo extends AppLocalizations { @override String get map_flags => '깃발'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => '여기에서 마커 공유'; @@ -1646,6 +1686,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get map_sharedPin => '공유 비밀번호'; + @override + String get map_sharedAt => '공유됨'; + @override String get map_joinRoom => '방에 참여'; @@ -3473,6 +3516,36 @@ class AppLocalizationsKo extends AppLocalizations { @override String get losLegendTerrain => '지형'; + @override + String get losBlockedSpotsTitle => '차단된 공간'; + + @override + String get losBlockedSpotsHint => '지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => '선택된 장애물'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => '빈도'; @@ -3908,4 +3981,40 @@ class AppLocalizationsKo extends AppLocalizations { @override String get translation_systemLanguage => '시스템 언어'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d62dc2e7..a2984b2e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -44,6 +44,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get common_close => 'Sluiten'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Bewerken'; @@ -432,9 +435,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Locatie opnemen in advertentie'; @override - String settings_multiAck(String value) { - return 'Meerdere bevestigingen: $value'; - } + String get settings_multiAck => 'Meerdere bevestigingen'; @override String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt'; @@ -442,6 +443,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_actions => 'Acties'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Verzend Advertentie'; @@ -1084,6 +1092,11 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channels_public => 'Openbaar'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privé'; @@ -1444,6 +1457,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_successes => 'Succesvol'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Pad verwijderen'; @@ -1545,6 +1561,12 @@ class AppLocalizationsNl extends AppLocalizations { return 'Nieuw: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Link openen?'; @@ -1626,6 +1648,24 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_flags => 'Vlaggen'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Deel marker hier'; @@ -1711,6 +1751,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_sharedPin => 'Gedeelde pin'; + @override + String get map_sharedAt => 'Gedeeld'; + @override String get map_joinRoom => 'Kamer Toetreden'; @@ -3649,6 +3692,37 @@ class AppLocalizationsNl extends AppLocalizations { @override String get losLegendTerrain => 'Terrein'; + @override + String get losBlockedSpotsTitle => 'Geplande plaatsen'; + + @override + String get losBlockedSpotsHint => + 'Tik op een geblokkeerd gebied om het op de kaart te markeren.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Geselecteerde obstakel'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frequentie'; @@ -4113,4 +4187,40 @@ class AppLocalizationsNl extends AppLocalizations { @override String get translation_systemLanguage => 'Taal van het systeem'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 25e2fe19..86e9422d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -44,6 +44,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get common_close => 'Zamknij'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Edytuj'; @@ -439,9 +442,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Uwzględnij lokalizację w ogłoszeniu'; @override - String settings_multiAck(String value) { - return 'Wielokrotne ACK: $value'; - } + String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru'; @override String get settings_telemetryModeUpdated => @@ -450,6 +451,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_actions => 'Działania'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Wyślij rozgłoszenie'; @@ -1104,6 +1112,11 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channels_public => 'Publiczny'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Prywatny'; @@ -1468,6 +1481,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_successes => 'Sukcesy'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Usuń ścieżkę'; @@ -1569,6 +1585,12 @@ class AppLocalizationsPl extends AppLocalizations { return 'Nieprzeczytane: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Otworzyć link?'; @@ -1650,6 +1672,24 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_flags => 'Flagi'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; @@ -1735,6 +1775,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_sharedPin => 'Udostępniona pinezka'; + @override + String get map_sharedAt => 'Udostępnione'; + @override String get map_joinRoom => 'Dołącz do pokoju'; @@ -3677,6 +3720,37 @@ class AppLocalizationsPl extends AppLocalizations { @override String get losLegendTerrain => 'Teren'; + @override + String get losBlockedSpotsTitle => 'Zablokowane miejsca'; + + @override + String get losBlockedSpotsHint => + 'Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Wybór przeszkody'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Częstotliwość'; @@ -4147,4 +4221,40 @@ class AppLocalizationsPl extends AppLocalizations { @override String get translation_systemLanguage => 'Język systemu'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 7a5ffe5d..9a5d50d8 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -44,6 +44,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get common_close => 'Fechar'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Editar'; @@ -436,9 +439,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Incluir localização no anúncio'; @override - String settings_multiAck(String value) { - return 'Múltiplas respostas de confirmação: $value'; - } + String get settings_multiAck => 'Multi-ACKs'; @override String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado'; @@ -446,6 +447,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_actions => 'Ações'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Enviar Publicidade'; @@ -1095,6 +1103,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channels_public => 'Público'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privado'; @@ -1455,6 +1468,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_successes => 'Sucessos'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Remover caminho'; @@ -1556,6 +1572,12 @@ class AppLocalizationsPt extends AppLocalizations { return 'Não lido: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Abrir link?'; @@ -1638,6 +1660,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_flags => 'Bandeiras'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Compartilhar marcador aqui'; @@ -1723,6 +1763,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_sharedPin => 'Pin compartilhado'; + @override + String get map_sharedAt => 'Compartilhado'; + @override String get map_joinRoom => 'Junte-se à Sala'; @@ -3662,6 +3705,37 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losLegendTerrain => 'Terreno'; + @override + String get losBlockedSpotsTitle => 'Locais ocupados'; + + @override + String get losBlockedSpotsHint => + 'Toque em um ponto bloqueado para destacá-lo no mapa.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Obstrução selecionada'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frequência'; @@ -4126,4 +4200,40 @@ class AppLocalizationsPt extends AppLocalizations { @override String get translation_systemLanguage => 'Idioma do sistema'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 16ab7061..aa34bedd 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -24,7 +24,7 @@ class AppLocalizationsRu extends AppLocalizations { String get common_cancel => 'Отмена'; @override - String get common_ok => 'Хорошо'; + String get common_ok => 'OK'; @override String get common_connect => 'Коннект'; @@ -44,6 +44,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get common_close => 'Закрыть'; + @override + String get common_done => 'Готово'; + @override String get common_edit => 'Изменить'; @@ -109,7 +112,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get scanner_title => 'MeshCore: Открытый исходный код'; + String get scanner_title => 'MeshCore Open'; @override String get connectionChoiceUsbLabel => 'USB'; @@ -436,9 +439,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Включить местоположение в объявление'; @override - String settings_multiAck(String value) { - return 'Мульти-ACK: $value'; - } + String get settings_multiAck => 'Несколько подтверждений'; @override String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен'; @@ -446,6 +447,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_actions => 'Действия'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Отправить анонсирование'; @@ -504,12 +512,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String settings_aboutVersion(String version) { - return 'MeshCore Open, версия $version'; + return 'MeshCore Open v$version'; } @override - String get settings_aboutLegalese => - 'Проект MeshCore с открытым исходным кодом, 2026 год'; + String get settings_aboutLegalese => '2026 MeshCore Open Source Project'; @override String get settings_aboutDescription => @@ -523,7 +530,7 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_infoName => 'Имя'; @override - String get settings_infoId => 'Идентификационный номер'; + String get settings_infoId => 'ID'; @override String get settings_infoStatus => 'Статус'; @@ -654,7 +661,7 @@ class AppLocalizationsRu extends AppLocalizations { String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Украинский'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => @@ -1096,6 +1103,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channels_public => 'Публичный'; + @override + String channels_via(String path) { + return 'через $path'; + } + @override String get channels_private => 'Приватный'; @@ -1148,7 +1160,7 @@ class AppLocalizationsRu extends AppLocalizations { String get channels_standardPublicPsk => 'Стандартный публичный PSK'; @override - String get channels_pskHex => 'PSK (шестнадцатеричный код)'; + String get channels_pskHex => 'PSK (Hex)'; @override String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; @@ -1401,7 +1413,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get debugFrame_textTypeCli => 'Интерфейс командной строки'; + String get debugFrame_textTypeCli => 'CLI'; @override String get debugFrame_textTypePlain => 'Обычный'; @@ -1459,6 +1471,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_successes => 'успешно'; + @override + String get chat_score => 'Оценка'; + @override String get chat_removePath => 'Удалить маршрут'; @@ -1561,6 +1576,12 @@ class AppLocalizationsRu extends AppLocalizations { return 'Непрочитанных: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Открыть ссылку?'; @@ -1642,6 +1663,24 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_flags => 'Флаги'; + @override + String get map_type => 'Тип'; + + @override + String get map_path => 'Путь'; + + @override + String get map_location => 'Местоположение'; + + @override + String get map_estLocation => 'Прибл. местоположение'; + + @override + String get map_publicKey => 'Публичный ключ'; + + @override + String get map_publicKeyPrefixHint => 'напр. ab12'; + @override String get map_shareMarkerHere => 'Поделиться меткой здесь'; @@ -1727,6 +1766,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_sharedPin => 'Общая метка'; + @override + String get map_sharedAt => 'Поделено'; + @override String get map_joinRoom => 'Присоединиться к комнате'; @@ -2047,7 +2089,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Просмотр телеметрии датчиков и системной статистики'; @override - String get repeater_cli => 'Интерфейс командной строки'; + String get repeater_cli => 'CLI'; @override String get repeater_cliSubtitle => 'Отправка команд репитеру'; @@ -3670,6 +3712,38 @@ class AppLocalizationsRu extends AppLocalizations { @override String get losLegendTerrain => 'Рельеф'; + @override + String get losBlockedSpotsTitle => 'Зарезервированные места'; + + @override + String get losBlockedSpotsHint => + 'Щелкните по заблокированной области, чтобы выделить ее на карте.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => + 'Выбранный объект, препятствующий движению'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Частота'; @@ -4143,4 +4217,40 @@ class AppLocalizationsRu extends AppLocalizations { @override String get translation_systemLanguage => 'Язык системы'; + + @override + String get background_serviceTitle => 'MeshCore работает'; + + @override + String get background_serviceText => 'Поддерживает BLE-соединение'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Удалено $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Не удалось удалить: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Не удалось обновить канал: $error'; + } + + @override + String get contact_typeChat => 'Чат'; + + @override + String get contact_typeRepeater => 'Ретранслятор'; + + @override + String get contact_typeRoom => 'Комната'; + + @override + String get contact_typeSensor => 'Датчик'; + + @override + String get contact_typeUnknown => 'Неизвестно'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 38e406d9..76ad7b65 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -44,6 +44,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get common_close => 'Zavrieť'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Upraviť'; @@ -430,9 +433,7 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu'; @override - String settings_multiAck(String value) { - return 'Viaceré ACK: $value'; - } + String get settings_multiAck => 'Viaceré ACK'; @override String get settings_telemetryModeUpdated => @@ -441,6 +442,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_actions => 'Možné akcie'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Odoslať reklamu'; @@ -1084,6 +1092,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channels_public => 'Veľké verejné'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Osobné'; @@ -1445,6 +1458,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_successes => 'Úspechy'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Odstrániť cestu'; @@ -1546,6 +1562,12 @@ class AppLocalizationsSk extends AppLocalizations { return 'Nezriadené: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Otvoriť odkaz?'; @@ -1627,6 +1649,24 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_flags => 'Zástavy'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Zdieľte značku tu'; @@ -1712,6 +1752,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_sharedPin => 'Zdieľaný PIN'; + @override + String get map_sharedAt => 'Zdieľané'; + @override String get map_joinRoom => 'Pripojiť miestnosť'; @@ -3644,6 +3687,37 @@ class AppLocalizationsSk extends AppLocalizations { @override String get losLegendTerrain => 'Terén'; + @override + String get losBlockedSpotsTitle => 'Zablokované miesta'; + + @override + String get losBlockedSpotsHint => + 'Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Vybraná prekážka'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frekvencia'; @@ -4108,4 +4182,40 @@ class AppLocalizationsSk extends AppLocalizations { @override String get translation_systemLanguage => 'Jazyk systému'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 112a1fec..4e02a5ef 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -44,6 +44,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_close => 'Zapri'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Uredi'; @@ -430,9 +433,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.'; @override - String settings_multiAck(String value) { - return 'Večkratni potrditvi: $value'; - } + String get settings_multiAck => 'Več potrdil'; @override String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen'; @@ -440,6 +441,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_actions => 'Akcije'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Pošlji Oglas'; @@ -1082,6 +1090,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_public => 'Javni'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Zasebni'; @@ -1442,6 +1455,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_successes => 'Uspešni'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Izbriši pot'; @@ -1541,6 +1557,12 @@ class AppLocalizationsSl extends AppLocalizations { return 'Nerešeno: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Odpreti povezavo?'; @@ -1623,6 +1645,24 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_flags => 'Zapestnice'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Delite točke tukaj.'; @@ -1707,6 +1747,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_sharedPin => 'Deljeno naslovno geslo'; + @override + String get map_sharedAt => 'Deljeno'; + @override String get map_joinRoom => 'Pridružiti sobo'; @@ -3640,6 +3683,37 @@ class AppLocalizationsSl extends AppLocalizations { @override String get losLegendTerrain => 'Teren'; + @override + String get losBlockedSpotsTitle => 'Zasedena parkirišča'; + + @override + String get losBlockedSpotsHint => + 'Dotaknite blokirano točko, da jo označite na zemljeplati.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Izbrano ovire'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frekvenca'; @@ -4107,4 +4181,40 @@ class AppLocalizationsSl extends AppLocalizations { @override String get translation_systemLanguage => 'Jezik sistema'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c5c744ae..ee028939 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -44,6 +44,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get common_close => 'Stänga'; + @override + String get common_done => 'Done'; + @override String get common_edit => 'Redigera'; @@ -428,9 +431,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen'; @override - String settings_multiAck(String value) { - return 'Flera ACK-meddelanden: $value'; - } + String get settings_multiAck => 'Flera bekräftelser'; @override String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat'; @@ -438,6 +439,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_actions => 'Åtgärder'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => 'Skicka Annons'; @@ -1075,6 +1083,11 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channels_public => 'Offentligt'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => 'Privat'; @@ -1437,6 +1450,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_successes => 'framgångar'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => 'Ta bort sökväg'; @@ -1535,6 +1551,12 @@ class AppLocalizationsSv extends AppLocalizations { return 'Olästa: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Öppna länk?'; @@ -1616,6 +1638,24 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_flags => 'Flaggor'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => 'Dela markeringen här'; @@ -1701,6 +1741,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_sharedPin => 'Delad PIN'; + @override + String get map_sharedAt => 'Delad'; + @override String get map_joinRoom => 'Gå med i rum'; @@ -3618,6 +3661,37 @@ class AppLocalizationsSv extends AppLocalizations { @override String get losLegendTerrain => 'Terräng'; + @override + String get losBlockedSpotsTitle => 'Reserverade platser'; + + @override + String get losBlockedSpotsHint => + 'Klicka på en markerad plats för att framhäva den på kartan.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Vald hinder'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Frekvens'; @@ -4081,4 +4155,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String get translation_systemLanguage => 'Språk för systemet'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 34a71b09..d0da30ad 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -44,6 +44,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get common_close => 'Закрити'; + @override + String get common_done => 'Готово'; + @override String get common_edit => 'Редагувати'; @@ -69,7 +72,7 @@ class AppLocalizationsUk extends AppLocalizations { String get common_continue => 'Продовжити'; @override - String get common_share => 'Поділитися'; + String get common_share => 'Поділитись'; @override String get common_copy => 'Копіювати'; @@ -121,7 +124,7 @@ class AppLocalizationsUk extends AppLocalizations { String get connectionChoiceTcpLabel => 'TCP'; @override - String get tcpScreenTitle => 'З\'єднатися через протокол TCP'; + String get tcpScreenTitle => 'Підключитись через TCP'; @override String get tcpHostLabel => 'IP-адреса'; @@ -147,7 +150,7 @@ class AppLocalizationsUk extends AppLocalizations { String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.'; @override - String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.'; + String get tcpErrorPortInvalid => 'Порт має бути в межах від 1 до 65535.'; @override String get tcpErrorUnsupported => @@ -155,11 +158,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get tcpErrorTimedOut => - 'З\'єднання TCP завершилося через закінчення часу очікування.'; + 'З\'єднання TCP завершилось через закінчення часу очікування.'; @override String tcpConnectionFailed(String error) { - return 'Не вдалося встановити з\'єднання TCP: $error'; + return 'Не вдалось встановити з\'єднання TCP: $error'; } @override @@ -167,7 +170,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get usbScreenSubtitle => - 'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.'; + 'Виберіть виявлений USB-пристрій і підключіть його безпосередньо до вашого вузла MeshCore.'; @override String get usbScreenStatus => 'Виберіть пристрій USB'; @@ -198,11 +201,11 @@ class AppLocalizationsUk extends AppLocalizations { String get usbErrorNotConnected => 'Немає підключених пристроїв USB.'; @override - String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.'; + String get usbErrorOpenFailed => 'Не вдалось відкрити вибране USB-пристрій.'; @override String get usbErrorConnectFailed => - 'Не вдалося підключитися до вибраного USB-пристрою.'; + 'Не вдалось підключитись до вибраного USB-пристрою.'; @override String get usbErrorUnsupported => @@ -220,7 +223,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.'; + 'З\'єднання не вдалось встановити. Переконайтесь, що пристрій має встановлене програмне забезпечення USB Companion.'; @override String get usbFallbackDeviceName => @@ -237,7 +240,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String usbConnectionFailed(String error) { - return 'Не вдалося встановити з\'єднання через USB: $error'; + return 'Не вдалось встановити з\'єднання через USB: $error'; } @override @@ -287,7 +290,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_chromeRequiredMessage => - 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; + 'Для підтримки Bluetooth у цьому вебзастосунку потрібен Google Chrome або браузер на базі Chromium.'; @override String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; @@ -305,7 +308,7 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_deviceInfo => 'Інформація про пристрій'; @override - String get settings_appSettings => 'Налаштування програми'; + String get settings_appSettings => 'Налаштування застосунку'; @override String get settings_appSettingsSubtitle => @@ -337,13 +340,13 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; @override - String get settings_location => 'Розташування'; + String get settings_location => 'Геопозиція'; @override - String get settings_locationSubtitle => 'GPS координати'; + String get settings_locationSubtitle => 'GPS-координати'; @override - String get settings_locationUpdated => 'Розташування оновлено'; + String get settings_locationUpdated => 'Геопозицію оновлено'; @override String get settings_locationBothRequired => 'Введіть широту та довготу.'; @@ -356,7 +359,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; + 'Вмикає автоматичне оновлення геопозиції через GPS.'; @override String get settings_locationIntervalSec => 'Інтервал для GPS (Секунди)'; @@ -383,11 +386,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/геопозицію в оголошеннях'; @override String get settings_privacyModeToggle => - 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; + 'Увімкніть режим приватності, щоб приховати своє ім\'я та геопозицію в оголошеннях.'; @override String get settings_privacyModeEnabled => 'Режим приватності увімкнено'; @@ -400,7 +403,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_privacySubtitle => - 'Керуйте інформацією, яку буде спільно використовуватися'; + 'Керуйте інформацією, яка буде спільно використовуватись'; @override String get settings_privacySettingsDescription => @@ -425,16 +428,14 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії'; @override - String get settings_advertLocation => 'Розміщення реклами'; + String get settings_advertLocation => 'Геопозиція в оголошенні'; @override String get settings_advertLocationSubtitle => - 'Включити місце розташування в оголошення'; + 'Включити геопозицію в оголошення'; @override - String settings_multiAck(String value) { - return 'Багатократне підтвердження: $value'; - } + String get settings_multiAck => 'Багато підтверджень'; @override String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено'; @@ -442,6 +443,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_actions => 'Дії'; + @override + String get settings_deleteAllPaths => 'Видалити всі шляхи'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Очистити всі дані шляхів у контактах.'; + @override String get settings_sendAdvertisement => 'Оголосити себе'; @@ -491,18 +499,18 @@ class AppLocalizationsUk extends AppLocalizations { 'Команди BLE, відповіді та необроблені дані'; @override - String get settings_appDebugLog => 'Журнал налагодження програми'; + String get settings_appDebugLog => 'Журнал налагодження застосунку'; @override String get settings_appDebugLogSubtitle => - 'Повідомлення налагодження програми'; + 'Повідомлення налагодження застосунку'; @override - String get settings_about => 'Про програму'; + String get settings_about => 'Про застосунок'; @override String settings_aboutVersion(String version) { - return 'MeshCore Open версії $version'; + return 'MeshCore Open v$version'; } @override @@ -584,7 +592,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get appSettings_title => 'Налаштування програми'; + String get appSettings_title => 'Налаштування застосунку'; @override String get appSettings_appearance => 'Вигляд'; @@ -719,14 +727,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_pathsWillNotBeCleared => - 'Шляхи не будуть очищатися автоматично.'; + 'Шляхи не будуть очищатись автоматично.'; @override String get appSettings_autoRouteRotation => 'Авторотація маршруту'; @override String get appSettings_autoRouteRotationSubtitle => - 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; + 'Чергувати найкращі шляхи та режим «через всю мережу» (flood)'; @override String get appSettings_autoRouteRotationEnabled => @@ -864,13 +872,13 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; @override - String get appSettings_unitsTitle => 'одиниці'; + String get appSettings_unitsTitle => 'Одиниці'; @override - String get appSettings_unitsMetric => 'Метричний (м / км)'; + String get appSettings_unitsMetric => 'Метричні (м / км)'; @override - String get appSettings_unitsImperial => 'Імперська (ft / mi)'; + String get appSettings_unitsImperial => 'Імперські (ft / mi)'; @override String get appSettings_noAreaSelected => 'Область не вибрано'; @@ -884,19 +892,20 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_debugCard => 'Налагодження'; @override - String get appSettings_appDebugLogging => 'Логування налагодження програми'; + String get appSettings_appDebugLogging => + 'Журналювання налагодження застосунку'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; + 'Записувати повідомлення налагодження застосунку в журнал для усунення несправностей.'; @override String get appSettings_appDebugLoggingEnabled => - 'Логування налагодження програми увімкнено'; + 'Журналювання налагодження застосунку увімкнено'; @override String get appSettings_appDebugLoggingDisabled => - 'Налагодження програми вимкнено.'; + 'Журналювання налагодження застосунку вимкнено.'; @override String get contacts_title => 'Контакти'; @@ -1043,14 +1052,14 @@ class AppLocalizationsUk extends AppLocalizations { String get contact_clearChat => 'Очистити чат'; @override - String get contact_teleBase => 'Базовий телебачення'; + String get contact_teleBase => 'Базова телеметрія'; @override String get contact_teleBaseSubtitle => 'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії'; @override - String get contact_teleLoc => 'Розташування телеметрії'; + String get contact_teleLoc => 'Геопозиція телеметрії'; @override String get contact_teleLocSubtitle => @@ -1084,11 +1093,16 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get channels_hashtagChannel => 'Канал з хештегом'; + String get channels_hashtagChannel => 'Хештег-канал'; @override String get channels_public => 'Публічний'; + @override + String channels_via(String path) { + return 'через $path'; + } + @override String get channels_private => 'Приватний'; @@ -1117,7 +1131,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Не вдалося видалити канал \"$name\"'; + return 'Не вдалось видалити канал \"$name\"'; } @override @@ -1151,7 +1165,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channels_pskMustBe32Hex => - 'PSK має складатися з 32 шістнадцяткових символів.'; + 'PSK має складатись з 32 шістнадцяткових символів.'; @override String channels_channelAdded(String name) { @@ -1196,24 +1210,24 @@ class AppLocalizationsUk extends AppLocalizations { String get channels_createPrivateChannelDesc => 'Захищено секретним ключем.'; @override - String get channels_joinPrivateChannel => 'Приєднатися до приватного каналу'; + String get channels_joinPrivateChannel => 'Приєднатись до приватного каналу'; @override String get channels_joinPrivateChannelDesc => 'Ввести секретний ключ вручну.'; @override - String get channels_joinPublicChannel => 'Приєднатися до публічного каналу'; + String get channels_joinPublicChannel => 'Приєднатись до публічного каналу'; @override String get channels_joinPublicChannelDesc => - 'Будь-хто може приєднатися до цього каналу.'; + 'Будь-хто може приєднатись до цього каналу.'; @override - String get channels_joinHashtagChannel => 'Приєднатися до каналу з хештегом'; + String get channels_joinHashtagChannel => 'Приєднатись до хештег-каналу'; @override String get channels_joinHashtagChannelDesc => - 'Будь-хто може приєднатися до каналів #hashtag.'; + 'Будь-хто може приєднатись до хештег-каналів.'; @override String get channels_scanQrCode => 'Сканувати QR-код'; @@ -1256,7 +1270,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get chat_location => 'Розташування'; + String get chat_location => 'Геопозиція'; @override String get chat_typeMessage => 'Введіть повідомлення...'; @@ -1317,7 +1331,7 @@ class AppLocalizationsUk extends AppLocalizations { String get gifPicker_noGifsFound => 'GIF не знайдено'; @override - String get gifPicker_failedLoad => 'Не вдалося завантажити GIF-файли'; + String get gifPicker_failedLoad => 'Не вдалось завантажити GIF-файли'; @override String get gifPicker_failedSearch => 'Пошук GIF не вдався'; @@ -1326,7 +1340,7 @@ class AppLocalizationsUk extends AppLocalizations { String get gifPicker_noInternet => 'Немає інтернет-з\'єднання'; @override - String get debugLog_appTitle => 'Журнал налагодження програми'; + String get debugLog_appTitle => 'Журнал налагодження застосунку'; @override String get debugLog_bleTitle => 'Журнал налагодження BLE'; @@ -1349,7 +1363,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get debugLog_enableInSettings => - 'Увімкніть налагодження програми в налаштуваннях'; + 'Увімкніть налагодження застосунку в налаштуваннях'; @override String get debugLog_frames => 'Кадри'; @@ -1420,31 +1434,31 @@ class AppLocalizationsUk extends AppLocalizations { String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; @override - String get chat_forceFloodMode => 'Примусово на всю мережу'; + String get chat_forceFloodMode => 'Примусово через всю мережу'; @override String get chat_recentAckPaths => - 'Недавні шляхи ACK (натисніть, щоб використати):'; + 'Підтверджені шляхи (натисніть, щоб використати):'; @override String get chat_pathHistoryFull => 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; @override - String get chat_hopSingular => 'Стрибок'; + String get chat_hopSingular => 'Перехід'; @override - String get chat_hopPlural => 'стрибків'; + String get chat_hopPlural => 'переходів'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'переходів', + many: 'переходів', + few: 'переходи', + one: 'перехід', ); return '$count $_temp0'; } @@ -1452,6 +1466,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_successes => 'Успішно'; + @override + String get chat_score => 'Оцінка'; + @override String get chat_removePath => 'Видалити шлях'; @@ -1481,11 +1498,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_floodModeSubtitle => - 'Використовувати перемикач маршрутизації в панелі програми'; + 'Використовувати перемикач маршрутизації в панелі застосунку'; @override String get chat_floodModeEnabled => - 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; + 'Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; @override String get chat_fullPath => 'Повний шлях'; @@ -1499,10 +1516,10 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( hopCount, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'переходів', + many: 'переходів', + few: 'переходи', + one: 'перехід', ); return 'Шлях встановлено: $hopCount $_temp0 - $status'; } @@ -1530,30 +1547,36 @@ class AppLocalizationsUk extends AppLocalizations { String get chat_compressOutgoingMessages => 'Стискати вихідні повідомлення'; @override - String get chat_floodForced => 'На всю мережу (примусово)'; + String get chat_floodForced => 'Через всю мережу (примусово)'; @override - String get chat_directForced => 'Прямий (примусово)'; + String get chat_directForced => 'Напряму (примусово)'; @override String chat_hopsForced(int count) { - return '$count стрибків (примусово)'; + return '$count переходів (примусово)'; } @override - String get chat_floodAuto => 'На всю мережу (авто)'; + String get chat_floodAuto => 'Через всю мережу (авто)'; @override - String get chat_direct => 'Прямий'; + String get chat_direct => 'Напряму'; @override - String get chat_poiShared => 'Точкою інтересу поділилися'; + String get chat_poiShared => 'Поділилися точкою інтересу'; @override String chat_unread(int count) { return 'Непрочитано: $count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => 'Відкрити посилання?'; @@ -1566,7 +1589,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String chat_couldNotOpenLink(String url) { - return 'Не вдалося відкрити посилання: $url'; + return 'Не вдалось відкрити посилання: $url'; } @override @@ -1582,12 +1605,11 @@ class AppLocalizationsUk extends AppLocalizations { String get map_losScreenTitle => 'Пряма видимість'; @override - String get map_noNodesWithLocation => - 'Немає вузлів з даними про розташування'; + String get map_noNodesWithLocation => 'Немає вузлів з даними про геопозицію'; @override String get map_nodesNeedGps => - 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; + 'Вузли мають надавати свої GPS координати,\nщоб з\'явитись на карті.'; @override String map_nodesCount(int count) { @@ -1625,7 +1647,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитись від цього пристрою?'; @override String get map_from => 'Від'; @@ -1637,10 +1659,28 @@ class AppLocalizationsUk extends AppLocalizations { String get map_flags => 'Прапорці'; @override - String get map_shareMarkerHere => 'Поділитися маркером тут'; + String get map_type => 'Тип'; @override - String get map_setAsMyLocation => 'Встановити моє місцезнаходження'; + String get map_path => 'Шлях'; + + @override + String get map_location => 'Геопозиція'; + + @override + String get map_estLocation => 'Орієнтовна геопозиція'; + + @override + String get map_publicKey => 'Публічний ключ'; + + @override + String get map_publicKeyPrefixHint => 'напр. ab12'; + + @override + String get map_shareMarkerHere => 'Поділитись маркером тут'; + + @override + String get map_setAsMyLocation => 'Встановити мою геопозицію'; @override String get map_pinLabel => 'Мітка піна'; @@ -1661,16 +1701,16 @@ class AppLocalizationsUk extends AppLocalizations { String get map_noChannelsAvailable => 'Немає доступних каналів'; @override - String get map_publicLocationShare => 'Поділитися в публічному місці'; + String get map_publicLocationShare => 'Поділитись в публічному місці'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.'; + return 'Ви збираєтесь поділитись геопозицією у $channelLabel. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.'; } @override String get map_connectToShareMarkers => - 'Підключіться до пристрою, щоб поділитися маркерами'; + 'Підключіться до пристрою, щоб поділитись маркерами'; @override String get map_filterNodes => 'Фільтрувати вузли'; @@ -1688,7 +1728,7 @@ class AppLocalizationsUk extends AppLocalizations { String get map_otherNodes => 'Інші вузли'; @override - String get map_showOverlaps => 'Перекриття ключа повторювача'; + String get map_showOverlaps => 'Перекриття ключів ретрансляторів'; @override String get map_keyPrefix => 'Префікс ключа'; @@ -1707,13 +1747,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showGuessedLocations => - 'Показати місцезнаходження передбачених вузлів'; + 'Показати геопозиції передбачених вузлів'; @override - String get map_showDiscoveryContacts => 'Показати контакти Відкриття'; + String get map_showDiscoveryContacts => 'Показати виявлені контакти'; @override - String get map_guessedLocation => 'Визначено місцезнаходження'; + String get map_guessedLocation => 'Передбачена геопозиція'; @override String get map_lastSeenTime => 'Час останньої активності'; @@ -1722,7 +1762,10 @@ class AppLocalizationsUk extends AppLocalizations { String get map_sharedPin => 'Спільний пін'; @override - String get map_joinRoom => 'Приєднатися до кімнати'; + String get map_sharedAt => 'Поділено'; + + @override + String get map_joinRoom => 'Приєднатись до кімнати'; @override String get map_manageRepeater => 'Керувати ретранслятором'; @@ -1734,13 +1777,13 @@ class AppLocalizationsUk extends AppLocalizations { String get map_runTrace => 'Виконати трасування шляху'; @override - String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом'; + String get map_runTraceWithReturnPath => 'Повернутись назад тим же шляхом'; @override String get map_removeLast => 'Видалити останній'; @override - String get map_pathTraceCancelled => 'Відмінується трасування шляху'; + String get map_pathTraceCancelled => 'Трасування шляху скасовано.'; @override String get mapCache_title => 'Офлайн-кеш карти'; @@ -1880,7 +1923,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get dialog_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитись від цього пристрою?'; @override String get login_repeaterLogin => 'Вхід у ретранслятор'; @@ -1916,10 +1959,10 @@ class AppLocalizationsUk extends AppLocalizations { String get login_routingMode => 'Режим маршрутизації'; @override - String get login_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; + String get login_autoUseSavedPath => 'Авто (збережений шлях)'; @override - String get login_forceFloodMode => 'Примусово на всю мережу'; + String get login_forceFloodMode => 'Примусово через всю мережу'; @override String get login_managePaths => 'Керувати шляхами'; @@ -1957,10 +2000,10 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибками', - many: 'стрибками', - few: 'стрибками', - one: 'стрибком', + other: 'переходами', + many: 'переходами', + few: 'переходами', + one: 'переходом', ); return 'Використання шляху з $count $_temp0'; } @@ -1973,7 +2016,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; + 'Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.'; @override String get path_hexPrefixExample => @@ -1984,7 +2027,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get path_helperMaxHops => - 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; + 'Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)'; @override String get path_selectFromContacts => 'Вибрати з контактів:'; @@ -2003,7 +2046,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get path_tooLong => 'Шлях занадто довгий. Максимум 64 стрибки.'; + String get path_tooLong => 'Шлях занадто довгий. Максимум 64 переходи.'; @override String get path_setPath => 'Встановити шлях'; @@ -2024,7 +2067,7 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_managementTools => 'Інструменти керування'; @override - String get repeater_guestTools => 'Інструменти для гостей'; + String get repeater_guestTools => 'Гостьові інструменти'; @override String get repeater_status => 'Статус'; @@ -2051,7 +2094,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_neighborsSubtitle => - 'Показати сусідів нульового стрибка.'; + 'Показати сусідів, доступних без ретрансляції.'; @override String get repeater_settings => 'Налаштування'; @@ -2077,7 +2120,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Авто (використовувати збережений шлях)'; @override - String get repeater_forceFloodMode => 'Примусово на всю мережу'; + String get repeater_forceFloodMode => 'Примусово через всю мережу'; @override String get repeater_pathManagement => 'Керування шляхами'; @@ -2154,17 +2197,17 @@ class AppLocalizationsUk extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, Через всю мережу: $flood, Прямі: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, Через всю мережу: $flood, Прямі: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'На всю мережу: $flood, Прямі: $direct'; + return 'Через всю мережу: $flood, Прямі: $direct'; } @override @@ -2223,7 +2266,7 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_codingRate => 'Швидкість кодування'; @override - String get repeater_locationSettings => 'Налаштування розташування'; + String get repeater_locationSettings => 'Налаштування геопозиції'; @override String get repeater_latitude => 'Широта'; @@ -2261,14 +2304,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/геопозицію в оголошеннях'; @override String get repeater_advertisementSettings => 'Налаштування оголошень'; @override String get repeater_localAdvertInterval => - 'Інтервал локальних оголошень (0 стрибків)'; + 'Інтервал локальних оголошень (без ретрансляції)'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2277,7 +2320,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Інтервал оголошень на всю мережу (flood)'; + 'Інтервал оголошень через всю мережу (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2304,7 +2347,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_regenerateIdentityKey => - 'Перегенерувати ключ ідентичності'; + 'Перегенерувати ключ ідентифікації'; @override String get repeater_regenerateIdentityKeySubtitle => @@ -2312,7 +2355,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Це створить нову ідентичність для ретранслятора. Продовжити?'; + 'Це створить нову ідентифікацію для ретранслятора. Продовжити?'; @override String get repeater_eraseFileSystem => 'Очистити файлову систему'; @@ -2599,7 +2642,7 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_cliQuickClockSync => 'Синхронізація годинника'; @override - String get repeater_cliQuickDiscovery => 'Відкрити сусідів'; + String get repeater_cliQuickDiscovery => 'Виявити сусідів'; @override String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; @@ -2641,7 +2684,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpSetFloodMax => - 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; + 'Встановлює максимальну кількість переходів для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; @override String get repeater_cliHelpSetIntThresh => @@ -2661,7 +2704,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; + 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення через всю мережу. Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetGuestPassword => @@ -2688,7 +2731,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpSetTxDelay => - 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; + 'Встановлює множник для часу роботи в режимі «через всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; @override String get repeater_cliHelpSetDirectTxDelay => @@ -2751,7 +2794,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; + '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «через всю мережу» (flood).'; @override String get repeater_cliHelpRegionLoad => @@ -2805,11 +2848,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpGpsAdvert => - 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; + 'Надає конфігурацію оголошення геопозиції вузла:\n- none : не включати геопозицію в оголошення\n- share : ділитись геопозицією GPS (з SensorManager)\n- prefs : оголошувати геопозицію, збережену в налаштуваннях'; @override String get repeater_cliHelpGpsAdvertSet => - 'Встановлює конфігурацію оголошення розташування.'; + 'Встановлює конфігурацію оголошення геопозиції.'; @override String get repeater_commandsListTitle => 'Список команд'; @@ -2828,7 +2871,7 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_bridge => 'Міст'; @override - String get repeater_logging => 'Логування'; + String get repeater_logging => 'Журналювання'; @override String get repeater_neighborsRepeaterOnly => 'Сусіди (Тільки ретранслятор)'; @@ -3218,7 +3261,7 @@ class AppLocalizationsUk extends AppLocalizations { String get channelPath_otherObservedPaths => 'Інші спостережувані шляхи'; @override - String get channelPath_repeaterHops => 'Стрибки ретранслятора'; + String get channelPath_repeaterHops => 'Переходи через ретранслятори'; @override String get channelPath_noHopDetails => @@ -3250,7 +3293,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get channelPath_noLocationData => 'Немає даних про розташування'; + String get channelPath_noLocationData => 'Немає даних про геопозицію'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -3266,19 +3309,19 @@ class AppLocalizationsUk extends AppLocalizations { String get channelPath_unknownPath => 'Невідомий'; @override - String get channelPath_floodPath => 'На всю мережу'; + String get channelPath_floodPath => 'Через всю мережу'; @override - String get channelPath_directPath => 'Прямий'; + String get channelPath_directPath => 'Напряму'; @override String channelPath_observedZeroOf(int total) { - return '0 з $total стрибків'; + return '0 з $total переходів'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed з $total стрибків'; + return '$observed з $total переходів'; } @override @@ -3306,7 +3349,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channelPath_noHopDetailsAvailable => - 'Деталі стрибків недоступні для цього пакету.'; + 'Деталі переходів недоступні для цього пакету.'; @override String get channelPath_unknownRepeater => 'Невідомий ретранслятор'; @@ -3319,17 +3362,17 @@ class AppLocalizationsUk extends AppLocalizations { @override String get community_createDesc => - 'Створити нову спільноту та поділитися через QR-код.'; + 'Створити нову спільноту та поділитись через QR-код.'; @override - String get community_join => 'Приєднатися'; + String get community_join => 'Приєднатись'; @override - String get community_joinTitle => 'Приєднатися до спільноти'; + String get community_joinTitle => 'Приєднатись до спільноти'; @override String community_joinConfirmation(String name) { - return 'Ви бажаєте приєднатися до спільноти «$name»?'; + return 'Ви бажаєте приєднатись до спільноти «$name»?'; } @override @@ -3365,16 +3408,16 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get community_qrTitle => 'Поділитися спільнотою'; + String get community_qrTitle => 'Поділитись спільнотою'; @override String community_qrInstructions(String name) { - return 'Відскануйте цей QR-код, щоб приєднатися до $name'; + return 'Відскануйте цей QR-код, щоб приєднатись до $name'; } @override String get community_hashtagPrivacyHint => - 'Канали хештегів спільноти доступні лише членам спільноти'; + 'Хештег-канали спільноти доступні лише членам спільноти'; @override String get community_invalidQrCode => 'Недійсний QR-код спільноти'; @@ -3431,11 +3474,11 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get community_regenerateSecret => 'Перегенерувати секрет'; + String get community_regenerateSecret => 'Перегенерувати секретний ключ'; @override String community_regenerateSecretConfirm(String name) { - return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; + return 'Перегенерувати секретний ключ для «$name»? Усі учасники матимуть відсканувати новий QR-код, щоб продовжити спілкування.'; } @override @@ -3443,20 +3486,20 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Секретний пароль для «$name» перегенеровано'; + return 'Секретний ключ для «$name» перегенеровано'; } @override - String get community_updateSecret => 'Оновити секрет'; + String get community_updateSecret => 'Оновити секретний ключ'; @override String community_secretUpdated(String name) { - return 'Зміну секрету для «$name» оновлено'; + return 'Секретний ключ для «$name» оновлено'; } @override String community_scanToUpdateSecret(String name) { - return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; + return 'Відскануйте новий QR-код, щоб оновити секретний ключ для «$name»'; } @override @@ -3464,7 +3507,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get community_addHashtagChannelDesc => - 'Додати канал хештегу для цієї спільноти'; + 'Додати хештег-канал для цієї спільноти'; @override String get community_selectCommunity => 'Вибрати спільноту'; @@ -3474,7 +3517,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get community_regularHashtagDesc => - 'Публічний хештег (будь-хто може приєднатися)'; + 'Публічний хештег (будь-хто може приєднатись)'; @override String get community_communityHashtag => 'Хештег спільноти'; @@ -3498,7 +3541,7 @@ class AppLocalizationsUk extends AppLocalizations { String get listFilter_latestMessages => 'Останні повідомлення'; @override - String get listFilter_heardRecently => 'Нещодавно чули'; + String get listFilter_heardRecently => 'Нещодавно почуті'; @override String get listFilter_az => 'А-Я'; @@ -3537,17 +3580,17 @@ class AppLocalizationsUk extends AppLocalizations { String get pathTrace_you => 'Ви'; @override - String get pathTrace_failed => 'Відстеження шляху не вдалося.'; + String get pathTrace_failed => 'Відстеження шляху не вдалось.'; @override String get pathTrace_notAvailable => 'Трасування шляху недоступне.'; @override - String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + String get pathTrace_refreshTooltip => 'Оновити трасування шляху'; @override String get pathTrace_someHopsNoLocation => - 'Одне або більше хмелів відсутнє місце розташування!'; + 'Один або декілька переходів не мають даних про геопозицію!'; @override String get pathTrace_clearTooltip => 'Очистити шлях'; @@ -3583,7 +3626,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String losCustomPointLabel(int index) { - return 'Спеціальний $index'; + return 'Власна точка $index'; } @override @@ -3648,7 +3691,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Недійсні дані про точки/висоту для розрахунку LOS.'; @override - String get losRenameCustomPoint => 'Перейменуйте спеціальну точку'; + String get losRenameCustomPoint => 'Перейменувати власну точку'; @override String get losPointName => 'Назва точки'; @@ -3672,6 +3715,37 @@ class AppLocalizationsUk extends AppLocalizations { @override String get losLegendTerrain => 'Рельєф'; + @override + String get losBlockedSpotsTitle => 'Заблоковані місця'; + + @override + String get losBlockedSpotsHint => + 'Натисніть на заблоковане місце, щоб виділити його на карті.'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => 'Вибраний об\'єкт перешкоди'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => 'Частота'; @@ -3698,10 +3772,10 @@ class AppLocalizationsUk extends AppLocalizations { String get contacts_ping => 'Пінгувати'; @override - String get contacts_repeaterPathTrace => 'Трасування шляху до повторювача'; + String get contacts_repeaterPathTrace => 'Трасування шляху до ретранслятора'; @override - String get contacts_repeaterPing => 'Пінгувати повторювач'; + String get contacts_repeaterPing => 'Пінгувати ретранслятор'; @override String get contacts_roomPathTrace => 'Трасування шляху до серверу кімнати'; @@ -3727,28 +3801,26 @@ class AppLocalizationsUk extends AppLocalizations { String get contacts_contactImported => 'Контакт було імпортовано.'; @override - String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати'; + String get contacts_contactImportFailed => 'Контакт не вдалось імпортувати'; @override - String get contacts_zeroHopAdvert => 'Реклама без перехоплення'; + String get contacts_zeroHopAdvert => 'Оголошення без ретрансляції'; @override - String get contacts_floodAdvert => 'Залив реклами'; + String get contacts_floodAdvert => 'Оголошення з ретрансляцією'; @override - String get contacts_copyAdvertToClipboard => - 'Копіювати оголошення в буфер обміну'; + String get contacts_copyAdvertToClipboard => 'Копіювати оголошення'; @override - String get contacts_addContactFromClipboard => - 'Додати контакт з буфера обміну'; + String get contacts_addContactFromClipboard => 'Додати контакт з буфера'; @override String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; @override String get contacts_ShareContactZeroHop => - 'Поділитися контактом за оголошенням'; + 'Поділитись контактом за оголошенням'; @override String get contacts_zeroHopContactAdvertSent => @@ -3756,15 +3828,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_zeroHopContactAdvertFailed => - 'Не вдалося надіслати контакт.'; + 'Не вдалось надіслати контакт.'; @override String get contacts_contactAdvertCopied => - 'Рекламу скопійовано до буфера обміну.'; + 'Оголошення скопійовано до буфера обміну.'; @override String get contacts_contactAdvertCopyFailed => - 'Копіювання оголошення в буфер обміну завершилося невдало'; + 'Копіювання оголошення в буфер обміну завершилось невдало'; @override String get notification_activityTitle => 'Активність MeshCore'; @@ -3802,7 +3874,7 @@ class AppLocalizationsUk extends AppLocalizations { locale: localeName, other: 'нових вузлів', many: 'нових вузлів', - few: 'нових вузли', + few: 'нові вузли', one: 'новий вузол', ); return '$count $_temp0'; @@ -3818,25 +3890,25 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_gpxExportRepeaters => - 'Експортувати ретранслятори / сервер кімнати до GPX'; + 'Експорт ретрансляторів і серверів кімнат у GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; + 'Експортує ретранслятори та сервери кімнат з геопозицією у файл GPX.'; @override - String get settings_gpxExportContacts => 'Експортувати супутників до GPX'; + String get settings_gpxExportContacts => 'Експорт контактів у GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Експортує супутників з місцезнаходженням у файл GPX.'; + 'Експортує контакти з геопозицією у файл GPX.'; @override - String get settings_gpxExportAll => 'Експортувати всі контакти до GPX'; + String get settings_gpxExportAll => 'Експорт усіх контактів у GPX'; @override String get settings_gpxExportAllSubtitle => - 'Експортує всі контакти з місцем розташування у файл GPX.'; + 'Експортує всі контакти з геопозицією у файл GPX.'; @override String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.'; @@ -3849,14 +3921,14 @@ class AppLocalizationsUk extends AppLocalizations { 'Не підтримується на вашому пристрої/операційній системі'; @override - String get settings_gpxExportError => 'Сталася помилка під час експорту.'; + String get settings_gpxExportError => 'Сталась помилка під час експорту.'; @override String get settings_gpxExportRepeatersRoom => - 'Місцезнаходження повторювача та сервера кімнати'; + 'Геопозиції ретрансляторів та серверів кімнат'; @override - String get settings_gpxExportChat => 'Місця супутників'; + String get settings_gpxExportChat => 'Геопозиції контактів'; @override String get settings_gpxExportAllContacts => 'Усі місця контактів'; @@ -3870,7 +3942,7 @@ class AppLocalizationsUk extends AppLocalizations { 'експорт даних карти meshcore-open у форматі GPX'; @override - String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори'; + String get snrIndicator_nearByRepeaters => 'Найближчі ретранслятори'; @override String get snrIndicator_lastSeen => 'Останній раз бачили'; @@ -3891,15 +3963,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contactsSettings_autoAddUsersSubtitle => - 'Дозволити супутникові автоматично додавати виявлених користувачів'; + 'Дозволити пристрою-компаньйону автоматично додавати виявлених користувачів'; @override String get contactsSettings_autoAddRepeatersTitle => - 'Автоматично додавати повторювачі'; + 'Автоматично додавати ретранслятори'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Дозволити супутнику автоматично додавати виявлені ретранслятори'; + 'Дозволити пристрою-компаньйону автоматично додавати виявлені ретранслятори'; @override String get contactsSettings_autoAddRoomServersTitle => @@ -3907,15 +3979,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.'; + 'Дозволити пристрою-компаньйону автоматично додавати виявлені сервери кімнат.'; @override String get contactsSettings_autoAddSensorsTitle => - 'Автоматично додавати датчики'; + 'Автоматично додавати сенсори'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Дозволити супровіднику автоматично додавати виявлені сенсори'; + 'Дозволити пристрою-компаньйону автоматично додавати виявлені сенсори'; @override String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше'; @@ -3965,33 +4037,33 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_jumpToOldestUnreadSubtitle => - 'При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.'; + 'При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.'; @override - String get appSettings_languageHu => 'Угорський'; + String get appSettings_languageHu => 'Угорська'; @override String get appSettings_languageJa => 'Японська'; @override - String get appSettings_languageKo => 'Кореєська'; + String get appSettings_languageKo => 'Корейська'; @override String get radioStats_tooltip => 'Статистика радіо та мережі'; @override - String get radioStats_screenTitle => 'Дані про радіостанції'; + String get radioStats_screenTitle => 'Статистика радіо'; @override String get radioStats_notConnected => - 'Підключіться до пристрою, щоб переглядати статистику радіопередач.'; + 'Підключіться до пристрою, щоб переглядати статистику радіо.'; @override String get radioStats_firmwareTooOld => - 'Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.'; + 'Статистика радіо вимагає прошивки пристрою-компаньйона версії 8 або новішої.'; @override - String get radioStats_waiting => 'Очікую на отримання даних…'; + String get radioStats_waiting => 'Очікування даних…'; @override String radioStats_noiseFloor(int noiseDbm) { @@ -4010,12 +4082,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String radioStats_txAir(int seconds) { - return 'Час трансляції на телеканалі TX (загальний): $seconds секунд'; + return 'Час в ефірі TX (загальний): $seconds секунд'; } @override String radioStats_rxAir(int seconds) { - return 'Загальний час використання RX: $seconds секунд'; + return 'Час в ефірі RX (загальний): $seconds секунд'; } @override @@ -4031,11 +4103,11 @@ class AppLocalizationsUk extends AppLocalizations { String get radioStats_stripWaiting => 'Отримано статистику радіо…'; @override - String get radioStats_settingsTile => 'Дані про радіостанції'; + String get radioStats_settingsTile => 'Статистика радіо'; @override String get radioStats_settingsSubtitle => - 'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.'; + 'Рівень шуму, RSSI, SNR та час в ефірі.'; @override String get translation_title => 'Переклад'; @@ -4058,14 +4130,14 @@ class AppLocalizationsUk extends AppLocalizations { String get translation_targetLanguage => 'Цільова мова'; @override - String get translation_useAppLanguage => 'Використовуйте мову додатку'; + String get translation_useAppLanguage => 'Використовувати мову застосунку'; @override String get translation_downloadedModelLabel => 'Завантажений шаблон'; @override String get translation_presetModelLabel => - 'Заздалегідь налаштований модель від Hugging Face'; + 'Попередньо налаштована модель з Hugging Face'; @override String get translation_manualUrlLabel => @@ -4101,7 +4173,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String translation_downloadFailed(String error) { - return 'Не вдалося завантажити: $error'; + return 'Не вдалось завантажити: $error'; } @override @@ -4146,4 +4218,40 @@ class AppLocalizationsUk extends AppLocalizations { @override String get translation_systemLanguage => 'Мова системи'; + + @override + String get background_serviceTitle => 'MeshCore працює'; + + @override + String get background_serviceText => 'Підтримує з\'єднання BLE'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Видалено $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Не вдалось видалити: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Не вдалось оновити канал: $error'; + } + + @override + String get contact_typeChat => 'Чат'; + + @override + String get contact_typeRepeater => 'Ретранслятор'; + + @override + String get contact_typeRoom => 'Кімната'; + + @override + String get contact_typeSensor => 'Сенсор'; + + @override + String get contact_typeUnknown => 'Невідомо'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6a63a4b3..ae94bbf4 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -44,6 +44,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get common_close => '关闭'; + @override + String get common_done => 'Done'; + @override String get common_edit => '编辑'; @@ -408,9 +411,7 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_advertLocationSubtitle => '在广告中包含位置'; @override - String settings_multiAck(String value) { - return '多重ACK:$value'; - } + String get settings_multiAck => '多重ACK'; @override String get settings_telemetryModeUpdated => '遥测模式已更新'; @@ -418,6 +419,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_actions => '操作'; + @override + String get settings_deleteAllPaths => 'Delete All Paths'; + + @override + String get settings_deleteAllPathsSubtitle => + 'Clear all path data from contacts.'; + @override String get settings_sendAdvertisement => '发送广播'; @@ -1023,6 +1031,11 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channels_public => '公共'; + @override + String channels_via(String path) { + return 'via $path'; + } + @override String get channels_private => '私有'; @@ -1370,6 +1383,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_successes => '成功'; + @override + String get chat_score => 'Score'; + @override String get chat_removePath => '移除路径'; @@ -1457,6 +1473,12 @@ class AppLocalizationsZh extends AppLocalizations { return '未读:$count'; } + @override + String get chat_markAsUnread => 'Mark as Unread'; + + @override + String get chat_newMessages => 'New messages'; + @override String get chat_openLink => '打开链接?'; @@ -1535,6 +1557,24 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_flags => '标志'; + @override + String get map_type => 'Type'; + + @override + String get map_path => 'Path'; + + @override + String get map_location => 'Location'; + + @override + String get map_estLocation => 'Est. Location'; + + @override + String get map_publicKey => 'Public Key'; + + @override + String get map_publicKeyPrefixHint => 'e.g. ab12'; + @override String get map_shareMarkerHere => '在此分享标记'; @@ -1618,6 +1658,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_sharedPin => '共享标记'; + @override + String get map_sharedAt => '已分享'; + @override String get map_joinRoom => '加入房间'; @@ -3375,6 +3418,36 @@ class AppLocalizationsZh extends AppLocalizations { @override String get losLegendTerrain => '地形'; + @override + String get losBlockedSpotsTitle => '被占用区域'; + + @override + String get losBlockedSpotsHint => '点击地图上的某个被遮盖的区域,以突出显示该区域。'; + + @override + String losBlockedSpotChip( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit • $obstruction $heightUnit'; + } + + @override + String get losSelectedObstructionTitle => '选择性阻碍'; + + @override + String losSelectedObstructionDetails( + String obstruction, + String heightUnit, + String distanceFromA, + String distanceUnit, + String distanceFromB, + ) { + return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).'; + } + @override String get losFrequencyLabel => '频率'; @@ -3783,4 +3856,40 @@ class AppLocalizationsZh extends AppLocalizations { @override String get translation_systemLanguage => '系统语言'; + + @override + String get background_serviceTitle => 'MeshCore running'; + + @override + String get background_serviceText => 'Keeping BLE connected'; + + @override + String appSettings_translationModelDeleted(String name) { + return 'Deleted $name'; + } + + @override + String appSettings_translationModelDeleteFailed(String error) { + return 'Failed to delete: $error'; + } + + @override + String channels_channelUpdateFailed(String error) { + return 'Failed to update channel: $error'; + } + + @override + String get contact_typeChat => 'Chat'; + + @override + String get contact_typeRepeater => 'Repeater'; + + @override + String get contact_typeRoom => 'Room'; + + @override + String get contact_typeSensor => 'Sensor'; + + @override + String get contact_typeUnknown => 'Unknown'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4ceb821f..3b124552 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Privacy modus is ingeschakeld", "settings_privacyModeDisabled": "Privacy modus is uitgeschakeld", "settings_actions": "Acties", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Verzend Advertentie", "settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden", "settings_advertisementSent": "Advertentie verzonden", @@ -1995,13 +1997,6 @@ "contact_lastSeen": "Laatst gezien", "contact_clearChat": "Chat leegmaken", "contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.", "appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route", "appSettings_maxRouteWeight": "Maximale gewicht voor de route", @@ -2217,5 +2212,87 @@ "repeater_cliHelpRegionListDenied": "Geeft een lijst van regio's die het verkeer tijdens overstromingen verbieden.", "repeater_cliHelpStatsPackets": "(Alleen voor seriële verbindingen) Toont statistieken op pakketniveau.", "repeater_cliHelpStatsRadio": "(Alleen voor serienummers) Toont radio-statistieken.", - "repeater_cliHelpStatsCore": "(Alleen voor seriële communicatie) Toont de belangrijkste firmware-statistieken." + "repeater_cliHelpStatsCore": "(Alleen voor seriële communicatie) Toont de belangrijkste firmware-statistieken.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Meerdere bevestigingen", + "map_sharedAt": "Gedeeld", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Geselecteerde obstakel", + "losBlockedSpotsHint": "Tik op een geblokkeerd gebied om het op de kaart te markeren.", + "losBlockedSpotsTitle": "Geplande plaatsen", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 4c28ab48..f8cc8358 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Tryb prywatności włączony", "settings_privacyModeDisabled": "Tryb prywatności wyłączony", "settings_actions": "Działania", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Wyślij rozgłoszenie", "settings_sendAdvertisementSubtitle": "Nadaj obecność teraz", "settings_advertisementSent": "Rozgłoszenie wysłane", @@ -2033,13 +2035,6 @@ "contact_settings": "Ustawienia kontaktowe", "contact_lastSeen": "Ostatnio widziany", "contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Początkowa waga trasy", "appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu", "appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek", @@ -2052,7 +2047,6 @@ "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany", - "settings_multiAck": "Wielokrotne ACK: {value}", "map_showOverlaps": "Nakładające się klucze przekaźników", "map_runTraceWithReturnPath": "Wróć tą samą ścieżką", "@radioStats_noiseFloor": { @@ -2255,5 +2249,87 @@ "repeater_cliHelpRegionListDenied": "Wymienia regiony, w których ruch związany z powodziami jest ograniczony.", "repeater_cliHelpStatsPackets": "(Tylko dla serialu) Prezentuje statystyki na poziomie pakietów.", "repeater_cliHelpStatsRadio": "(Tylko serial) Prezentuje statystyki dotyczące nadawania radiowego.", - "repeater_cliHelpStatsCore": "(Tylko wersja serialowa) Wyświetla podstawowe statystyki o oprogramowaniu." + "repeater_cliHelpStatsCore": "(Tylko wersja serialowa) Wyświetla podstawowe statystyki o oprogramowaniu.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Wielokrotne potwierdzenia odbioru", + "map_sharedAt": "Udostępnione", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losSelectedObstructionTitle": "Wybór przeszkody", + "losBlockedSpotsTitle": "Zablokowane miejsca", + "losBlockedSpotsHint": "Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 74345da9..4e79f619 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Modo de privacidade ativado", "settings_privacyModeDisabled": "Modo de privacidade desativado", "settings_actions": "Ações", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Enviar Publicidade", "settings_sendAdvertisementSubtitle": "Presença de transmissão agora", "settings_advertisementSent": "Anúncio enviado", @@ -1995,13 +1997,6 @@ "contact_telemetry": "Telemetria", "contact_settings": "Configurações de Contato", "contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Peso Inicial da Rota", "appSettings_maxRouteWeight": "Peso Máximo da Rota", "appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.", @@ -2015,6 +2010,7 @@ "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Modo de telemetria atualizado", "settings_multiAck": "Múltiplas respostas de confirmação: {value}", + "settings_multiAck": "Multi-ACKs", "map_showOverlaps": "Sobreposições da Chave Repeater", "map_runTraceWithReturnPath": "Retornar ao mesmo caminho.", "@radioStats_noiseFloor": { @@ -2217,5 +2213,86 @@ "repeater_cliHelpRegionListDenied": "Lista as regiões que restringem o tráfego em áreas de risco de inundações.", "repeater_cliHelpStatsPackets": "(Apenas para séries) Apresenta estatísticas em nível de pacotes.", "repeater_cliHelpStatsRadio": "(Apenas para transmissões em série) Exibe estatísticas de rádio.", - "repeater_cliHelpStatsCore": "(Apenas para dispositivos em série) Exibe estatísticas básicas do firmware." + "repeater_cliHelpStatsCore": "(Apenas para dispositivos em série) Exibe estatísticas básicas do firmware.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "map_sharedAt": "Compartilhado", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsTitle": "Locais ocupados", + "losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.", + "losSelectedObstructionTitle": "Obstrução selecionada", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 33b5b7df..f4efb887 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -13,7 +13,7 @@ "nav_channels": "Каналы", "nav_map": "Карта", "common_cancel": "Отмена", - "common_ok": "Хорошо", + "common_ok": "OK", "common_connect": "Коннект", "common_unknownDevice": "Неизвестное устройство", "common_save": "Сохранить", @@ -39,7 +39,7 @@ "common_notAvailable": "—", "common_voltageValue": "{volts} В", "common_percentValue": "{percent}%", - "scanner_title": "MeshCore: Открытый исходный код", + "scanner_title": "MeshCore Open", "scanner_scanning": "Поиск устройств...", "scanner_connecting": "Подключение...", "scanner_disconnecting": "Отключение...", @@ -81,6 +81,8 @@ "settings_privacyModeEnabled": "Режим конфиденциальности включен", "settings_privacyModeDisabled": "Режим конфиденциальности выключен", "settings_actions": "Действия", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Отправить анонсирование", "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", "settings_advertisementSent": "Анонсирование отправлено", @@ -98,11 +100,11 @@ "settings_appDebugLog": "Журнал отладки приложения", "settings_appDebugLogSubtitle": "Сообщения отладки приложения", "settings_about": "О программе", - "settings_aboutVersion": "MeshCore Open, версия {version}", - "settings_aboutLegalese": "Проект MeshCore с открытым исходным кодом, 2026 год", + "settings_aboutVersion": "MeshCore Open v{version}", + "settings_aboutLegalese": "2026 MeshCore Open Source Project", "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", "settings_infoName": "Имя", - "settings_infoId": "Идентификационный номер", + "settings_infoId": "ID", "settings_infoStatus": "Статус", "settings_infoBattery": "Батарея", "settings_infoPublicKey": "Публичный ключ", @@ -245,7 +247,7 @@ "channels_channelName": "Имя канала", "channels_usePublicChannel": "Использовать публичный канал", "channels_standardPublicPsk": "Стандартный публичный PSK", - "channels_pskHex": "PSK (шестнадцатеричный код)", + "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Сгенерировать случайный PSK", "channels_enterChannelName": "Введите имя канала", "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", @@ -317,7 +319,7 @@ "debugFrame_timestamp": "- Временная метка: {timestamp}", "debugFrame_flags": "- Флаги: 0x{value}", "debugFrame_textType": "- Тип текста: {type} ({label})", - "debugFrame_textTypeCli": "Интерфейс командной строки", + "debugFrame_textTypeCli": "CLI", "debugFrame_textTypePlain": "Обычный", "debugFrame_text": "- Текст: \"{text}\"", "debugFrame_hexDump": "Шестнадцатеричный дамп:", @@ -476,7 +478,7 @@ "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", "repeater_telemetry": "Телеметрия", "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", - "repeater_cli": "Интерфейс командной строки", + "repeater_cli": "CLI", "repeater_cliSubtitle": "Отправка команд репитеру", "repeater_neighbors": "Соседи", "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", @@ -560,88 +562,15 @@ "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", "repeater_confirm": "Подтвердить", "repeater_settingsSaved": "Настройки успешно сохранены", - "repeater_rxGain": "Увеличенная эффективность RX", - "repeater_rxGainHelper": "Более высокая чувствительность, больший ток потребления (только для SX1262/SX1268)", - "repeater_refreshRxGain": "Обновите усиление RX", - "repeater_multiAcks": "Несколько подтверждений", - "repeater_multiAcksSubtitle": "Обеспечьте доставку сообщений по нескольким каналам для повышения эффективности.", - "repeater_refreshMultiAcks": "Обновление нескольких подтверждений", - "repeater_networkHealth": "Состояние сети", - "repeater_loopDetect": "Обнаружение циклов", - "repeater_loopDetectHelper": "Создайте пакеты данных, которые выглядят как циклы маршрутизации.", - "repeater_loopDetectOff": "Отключено", - "repeater_loopDetectMinimal": "Минимальный", - "repeater_loopDetectModerate": "Умеренный", - "repeater_loopDetectStrict": "Строгий", - "repeater_dutyCycle": "Цикл работы", - "repeater_dutyCycleHelper": "Максимальный процент времени, выделенного на трансляцию.", - "repeater_dutyCyclePercent": "{percent}%", - "@repeater_dutyCyclePercent": { - "placeholders": { - "percent": { - "type": "int" - } - } - }, - "repeater_ownerInfo": "Информация о операторе", - "repeater_ownerInfoHelper": "Общая метаинформация для этого ретранслятора", - "repeater_refreshOwnerInfo": "Обновить информацию о операторе", - "repeater_floodMax": "Максимальное количество прыжков при наводнении", - "repeater_floodMaxHelper": "Максимальное количество пакетов, которые могут быть отправлены в одном потоке (0-64)", - "repeater_advancedSettings": "Продвинутый", - "repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов", - "repeater_pathHashMode": "Режим хеширования пути", - "repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.", - "repeater_txDelay": "Задержка в работе системы Flood TX", - "repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.", - "repeater_directTxDelay": "Прямая задержка сигнала TX", - "repeater_directTxDelayHelper": "Передача промежуточных данных для прямого (немассового) трафика, в качестве коэффициента, равного времени передачи пакета (от 0 до 2, по умолчанию 0,3).", - "repeater_intThresh": "Пороговое значение помех", - "repeater_intThreshHelper": "Порог устанавливается для калибровки уровня шума радио, чтобы оно отсеивало помехи, превышающие этот уровень. Значение \"0\" означает отключение – используйте только в случае, если вы наблюдаете ошибки при приеме сигнала в шумном диапазоне.", - "repeater_agcResetInterval": "Интервал сброса AGC", - "repeater_agcResetIntervalHelper": "Как часто следует сбрасывать автоматическую регулировку усиления радио, чтобы вернуться к нормальному состоянию после заклинивания? Интервал сброса составляет несколько секунд, кратный 4. Отключение периодического сброса осуществляется с помощью параметра 0.", - "repeater_actionsTitle": "Действия", - "repeater_sendAdvert": "Отправить объявление о наводнении", - "repeater_sendAdvertSubtitle": "Разместите рекламу о наводнении в эфире по всей сети.", - "repeater_sendAdvertZeroHop": "Опубликуйте рекламу, не требующую промежуточного распространения.", - "repeater_sendAdvertZeroHopSubtitle": "Разместите рекламу, распространяемую одним способом (без использования ретрансляторов).", - "repeater_clockSync": "Синхронизировать время сейчас", - "repeater_clockSyncSubtitle": "Установите время на вашем телефоне, чтобы оно совпадало со временем ретранслятора.", - "repeater_actionSucceeded": "{action} succeeded", - "@repeater_actionSucceeded": { - "placeholders": { - "action": { - "type": "String" - } - } - }, - "repeater_actionFailed": "{action} failed: {error}", - "@repeater_actionFailed": { - "placeholders": { - "action": { - "type": "String" - }, - "error": { - "type": "String" - } - } - }, - "repeater_settingsSavedRebootNeeded": "Настройки сохранены — перезагрузите ретранслятор, чтобы применить их.", - "repeater_settingsPartialFailure": "Некоторые настройки не удалось применить: {failures}", - "@repeater_settingsPartialFailure": { - "placeholders": { - "failures": { - "type": "String" - } - } - }, "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", "repeater_refreshBasicSettings": "Обновить основные настройки", "repeater_refreshRadioSettings": "Обновить настройки радио", "repeater_refreshTxPower": "Обновить мощность передачи", + "repeater_refreshLocationSettings": "Обновить настройки местоположения", "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", "repeater_refreshGuestAccess": "Обновить гостевой доступ", "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", + "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", "repeater_refreshed": "{label} обновлён", "repeater_errorRefreshing": "Ошибка обновления {label}", "repeater_cliTitle": "CLI репитера", @@ -877,7 +806,7 @@ "contacts_contactImportFailed": "Контакт не удалось импортировать", "contacts_invalidAdvertFormat": "Недействительные контактные данные", "contacts_zeroHopAdvert": "Реклама Zero Hop", - "appSettings_languageUk": "Украинский", + "appSettings_languageUk": "Українська", "appSettings_enableMessageTracing": "Включить трассировку сообщений", "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", "contacts_floodAdvert": "Рекламный поток", @@ -1235,13 +1164,6 @@ "contact_clearChat": "Очистить чат", "contact_lastSeen": "Последний раз видели", "contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута", "appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.", "appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов", @@ -1254,7 +1176,6 @@ "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Режим телеметрии обновлен", - "settings_multiAck": "Мульти-ACK: {value}", "map_showOverlaps": "Перекрытия ключа повтора", "map_runTraceWithReturnPath": "Вернуться обратно по тому же пути", "@radioStats_noiseFloor": { @@ -1387,6 +1308,170 @@ "repeater_guest": "Информация о ретрансляторе", "room_guest": "Информация о сервере", "repeater_guestTools": "Инструменты для гостей", + "common_done": "Готово", + "background_serviceTitle": "MeshCore работает", + "background_serviceText": "Поддерживает BLE-соединение", + "appSettings_translationModelDeleted": "Удалено {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Не удалось удалить: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Не удалось обновить канал: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Тип", + "map_path": "Путь", + "map_location": "Местоположение", + "map_estLocation": "Прибл. местоположение", + "map_publicKey": "Публичный ключ", + "map_publicKeyPrefixHint": "напр. ab12", + "contact_typeChat": "Чат", + "contact_typeRepeater": "Ретранслятор", + "contact_typeRoom": "Комната", + "contact_typeSensor": "Датчик", + "contact_typeUnknown": "Неизвестно", + "channels_via": "через {path}", + "chat_score": "Оценка", + "settings_multiAck": "Несколько подтверждений", + "map_sharedAt": "Поделено", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.", + "losBlockedSpotsTitle": "Зарезервированные места", + "losSelectedObstructionTitle": "Выбранный объект, препятствующий движению", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).", + "repeater_rxGain": "Увеличенная эффективность RX", + "repeater_rxGainHelper": "Более высокая чувствительность, больший ток потребления (только для SX1262/SX1268)", + "repeater_refreshRxGain": "Обновите усиление RX", + "repeater_multiAcks": "Несколько подтверждений", + "repeater_multiAcksSubtitle": "Обеспечьте доставку сообщений по нескольким каналам для повышения эффективности.", + "repeater_refreshMultiAcks": "Обновление нескольких подтверждений", + "repeater_networkHealth": "Состояние сети", + "repeater_loopDetect": "Обнаружение циклов", + "repeater_loopDetectHelper": "Создайте пакеты данных, которые выглядят как циклы маршрутизации.", + "repeater_loopDetectOff": "Отключено", + "repeater_loopDetectMinimal": "Минимальный", + "repeater_loopDetectModerate": "Умеренный", + "repeater_loopDetectStrict": "Строгий", + "repeater_dutyCycle": "Цикл работы", + "repeater_dutyCycleHelper": "Максимальный процент времени, выделенного на трансляцию.", + "repeater_dutyCyclePercent": "{percent}%", + "@repeater_dutyCyclePercent": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "repeater_ownerInfo": "Информация о операторе", + "repeater_ownerInfoHelper": "Общая метаинформация для этого ретранслятора", + "repeater_refreshOwnerInfo": "Обновить информацию о операторе", + "repeater_floodMax": "Максимальное количество прыжков при наводнении", + "repeater_floodMaxHelper": "Максимальное количество пакетов, которые могут быть отправлены в одном потоке (0-64)", + "repeater_advancedSettings": "Продвинутый", + "repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов", + "repeater_pathHashMode": "Режим хеширования пути", + "repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.", + "repeater_txDelay": "Задержка в работе системы Flood TX", + "repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.", + "repeater_directTxDelay": "Прямая задержка сигнала TX", + "repeater_directTxDelayHelper": "Передача промежуточных данных для прямого (немассового) трафика, в качестве коэффициента, равного времени передачи пакета (от 0 до 2, по умолчанию 0,3).", + "repeater_intThresh": "Пороговое значение помех", + "repeater_intThreshHelper": "Порог устанавливается для калибровки уровня шума радио, чтобы оно отсеивало помехи, превышающие этот уровень. Значение \"0\" означает отключение – используйте только в случае, если вы наблюдаете ошибки при приеме сигнала в шумном диапазоне.", + "repeater_agcResetInterval": "Интервал сброса AGC", + "repeater_agcResetIntervalHelper": "Как часто следует сбрасывать автоматическую регулировку усиления радио, чтобы вернуться к нормальному состоянию после заклинивания? Интервал сброса составляет несколько секунд, кратный 4. Отключение периодического сброса осуществляется с помощью параметра 0.", + "repeater_actionsTitle": "Действия", + "repeater_sendAdvert": "Отправить объявление о наводнении", + "repeater_sendAdvertSubtitle": "Разместите рекламу о наводнении в эфире по всей сети.", + "repeater_sendAdvertZeroHop": "Опубликуйте рекламу, не требующую промежуточного распространения.", + "repeater_sendAdvertZeroHopSubtitle": "Разместите рекламу, распространяемую одним способом (без использования ретрансляторов).", + "repeater_clockSync": "Синхронизировать время сейчас", + "repeater_clockSyncSubtitle": "Установите время на вашем телефоне, чтобы оно совпадало со временем ретранслятора.", + "repeater_actionSucceeded": "{action} succeeded", + "@repeater_actionSucceeded": { + "placeholders": { + "action": { + "type": "String" + } + } + }, + "repeater_actionFailed": "{action} failed: {error}", + "@repeater_actionFailed": { + "placeholders": { + "action": { + "type": "String" + }, + "error": { + "type": "String" + } + } + }, + "repeater_settingsSavedRebootNeeded": "Настройки сохранены — перезагрузите ретранслятор, чтобы применить их.", + "repeater_settingsPartialFailure": "Некоторые настройки не удалось применить: {failures}", + "@repeater_settingsPartialFailure": { + "placeholders": { + "failures": { + "type": "String" + } + } + }, + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "@common_percentValue": { "placeholders": { "percent": { diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ca2bffc7..762b6b96 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Ochranný režim je povolený.", "settings_privacyModeDisabled": "Ochranný režim je vypnutý", "settings_actions": "Možné akcie", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Odoslať reklamu", "settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.", "settings_advertisementSent": "Reklama odeslaná", @@ -1995,13 +1997,6 @@ "contact_lastSeen": "Naposledy videný", "contact_teleBase": "Báza telemetrie", "contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.", "appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty", "appSettings_initialRouteWeight": "Počiatočná váha trasy", @@ -2014,7 +2009,7 @@ "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný", - "settings_multiAck": "Viaceré ACK: {value}", + "settings_multiAck": "Viaceré ACK", "map_showOverlaps": "Prekrývanie opakovača kľúča", "map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.", "@radioStats_noiseFloor": { @@ -2217,5 +2212,86 @@ "repeater_cliHelpRegionListDenied": "Zoznam oblastí, ktoré zakazujú premávku v dôsledku povodní.", "repeater_cliHelpStatsPackets": "(Len pre sériové záznamy) Zobrazuje štatistiky na úrovni paketov.", "repeater_cliHelpStatsRadio": "(Len pre sériu) Zobrazuje údaje o rádiových staniciach.", - "repeater_cliHelpStatsCore": "(Len pre sériové modely) Zobrazuje základné štatistiky firmvéru." + "repeater_cliHelpStatsCore": "(Len pre sériové modely) Zobrazuje základné štatistiky firmvéru.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "map_sharedAt": "Zdieľané", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsTitle": "Zablokované miesta", + "losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.", + "losSelectedObstructionTitle": "Vybraná prekážka", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 621646f3..06ca1451 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Privatni način je omogočen.", "settings_privacyModeDisabled": "Privatni način je onemogočen.", "settings_actions": "Akcije", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Pošlji Oglas", "settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah", "settings_advertisementSent": "Oglas poslan", @@ -1995,13 +1997,6 @@ "contact_teleEnv": "Okolje telemetrije", "contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev", "contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.", "appSettings_initialRouteWeight": "Izvirna teža poti", "appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti", @@ -2013,7 +2008,6 @@ "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil", "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno", "path_routeWeight": "{weight}/{max}", - "settings_multiAck": "Večkratni potrditvi: {value}", "settings_telemetryModeUpdated": "Način telemetrije posodobljen", "map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja", "map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.", @@ -2217,5 +2211,87 @@ "repeater_cliHelpRegionListDenied": "Navaja regije, ki preprečujejo promet zaradi poplav.", "repeater_cliHelpStatsPackets": "(Samo za serijske povezave) Prikazuje statistiko na nivoju paketov.", "repeater_cliHelpStatsRadio": "(Samo za serije) Prikazuje statistične podatke o radiju.", - "repeater_cliHelpStatsCore": "(Samo za serijske naprave) Prikazuje osnovne statistične podatke." + "repeater_cliHelpStatsCore": "(Samo za serijske naprave) Prikazuje osnovne statistične podatke.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Več potrdil", + "map_sharedAt": "Deljeno", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.", + "losSelectedObstructionTitle": "Izbrano ovire", + "losBlockedSpotsTitle": "Zasedena parkirišča", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 463d41c8..2a9a9ae7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -104,6 +104,8 @@ "settings_privacyModeEnabled": "Privatläget är aktiverat", "settings_privacyModeDisabled": "Privatläge är avstängt", "settings_actions": "Åtgärder", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "Skicka Annons", "settings_sendAdvertisementSubtitle": "Sändning finns nu", "settings_advertisementSent": "Annons skickad", @@ -1995,13 +1997,6 @@ "contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri", "contact_teleLoc": "Telemetridata plats", "contact_teleLocSubtitle": "Tillåt delning av platsdata", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar", "appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten", "appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.", @@ -2217,5 +2212,87 @@ "repeater_cliHelpRegionListDenied": "Listar områden där trafik på grund av översvämningar är förbjuden.", "repeater_cliHelpStatsPackets": "(Endast för seriell kommunikation) Visar statistik på paketnivå.", "repeater_cliHelpStatsRadio": "(Enbart för serier) Visar radiostatistik.", - "repeater_cliHelpStatsCore": "(Enbart för seriell kommunikation) Visar grundläggande firmware-statistik." + "repeater_cliHelpStatsCore": "(Enbart för seriell kommunikation) Visar grundläggande firmware-statistik.", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "settings_multiAck": "Flera bekräftelser", + "map_sharedAt": "Delad", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losBlockedSpotsTitle": "Reserverade platser", + "losSelectedObstructionTitle": "Vald hinder", + "losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 19843a8c..8b137ebe 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,5 +1,5 @@ { - "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", + "channels_channelDeleteFailed": "Не вдалось видалити канал \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -18,6 +18,7 @@ "common_save": "Зберегти", "common_delete": "Видалити", "common_close": "Закрити", + "common_done": "Готово", "common_edit": "Редагувати", "common_add": "Додати", "common_settings": "Налаштування", @@ -26,7 +27,7 @@ "common_disconnected": "Відключено", "common_create": "Створити", "common_continue": "Продовжити", - "common_share": "Поділитися", + "common_share": "Поділитись", "common_copy": "Копіювати", "common_retry": "Повторити", "common_hide": "Приховати", @@ -81,7 +82,7 @@ "device_meshcore": "MeshCore", "settings_title": "Налаштування", "settings_deviceInfo": "Інформація про пристрій", - "settings_appSettings": "Налаштування програми", + "settings_appSettings": "Налаштування застосунку", "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", "settings_nodeSettings": "Налаштування вузла", "settings_nodeName": "Ім'я вузла", @@ -91,19 +92,21 @@ "settings_radioSettings": "Налаштування радіо", "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", "settings_radioSettingsUpdated": "Налаштування радіо оновлено", - "settings_location": "Розташування", - "settings_locationSubtitle": "GPS координати", - "settings_locationUpdated": "Розташування оновлено", + "settings_location": "Геопозиція", + "settings_locationSubtitle": "GPS-координати", + "settings_locationUpdated": "Геопозицію оновлено", "settings_locationBothRequired": "Введіть широту та довготу.", "settings_locationInvalid": "Некоректна широта або довгота.", "settings_latitude": "Широта", "settings_longitude": "Довгота", "settings_privacyMode": "Режим приватності", - "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", - "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", + "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", + "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.", "settings_privacyModeEnabled": "Режим приватності увімкнено", "settings_privacyModeDisabled": "Режим приватності вимкнено", "settings_actions": "Дії", + "settings_deleteAllPaths": "Видалити всі шляхи", + "settings_deleteAllPathsSubtitle": "Очистити всі дані шляхів у контактах.", "settings_sendAdvertisement": "Оголосити себе", "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", "settings_advertisementSent": "Оголошення надіслано", @@ -118,10 +121,10 @@ "settings_debug": "Налагодження", "settings_bleDebugLog": "Журнал налагодження BLE", "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", - "settings_appDebugLog": "Журнал налагодження програми", - "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", - "settings_about": "Про програму", - "settings_aboutVersion": "MeshCore Open версії {version}", + "settings_appDebugLog": "Журнал налагодження застосунку", + "settings_appDebugLogSubtitle": "Повідомлення налагодження застосунку", + "settings_about": "Про застосунок", + "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { "version": { @@ -156,7 +159,7 @@ } } }, - "appSettings_title": "Налаштування програми", + "appSettings_title": "Налаштування застосунку", "appSettings_appearance": "Вигляд", "appSettings_theme": "Тема", "appSettings_themeSystem": "Системна", @@ -194,9 +197,9 @@ "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", - "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", + "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатись автоматично.", "appSettings_autoRouteRotation": "Авторотація маршруту", - "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", + "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «через всю мережу» (flood)", "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", "appSettings_battery": "Батарея", @@ -251,10 +254,10 @@ } }, "appSettings_debugCard": "Налагодження", - "appSettings_appDebugLogging": "Логування налагодження програми", - "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", - "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", - "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", + "appSettings_appDebugLogging": "Журналювання налагодження застосунку", + "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження застосунку в журнал для усунення несправностей.", + "appSettings_appDebugLoggingEnabled": "Журналювання налагодження застосунку увімкнено", + "appSettings_appDebugLoggingDisabled": "Журналювання налагодження застосунку вимкнено.", "contacts_title": "Контакти", "contacts_noContacts": "Контактів не знайдено.", "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", @@ -338,8 +341,9 @@ } } }, - "channels_hashtagChannel": "Канал з хештегом", + "channels_hashtagChannel": "Хештег-канал", "channels_public": "Публічний", + "channels_via": "через {path}", "channels_private": "Приватний", "channels_publicChannel": "Публічний канал", "channels_privateChannel": "Приватний канал", @@ -371,7 +375,7 @@ "channels_pskHex": "PSK (шестнадцяткова система)", "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", "channels_enterChannelName": "Будь ласка, введіть назву каналу", - "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", + "channels_pskMustBe32Hex": "PSK має складатись з 32 шістнадцяткових символів.", "channels_channelAdded": "Канал «{name}» додано", "@channels_channelAdded": { "placeholders": { @@ -422,7 +426,7 @@ } } }, - "chat_location": "Розташування", + "chat_location": "Геопозиція", "chat_sendMessageTo": "Надіслати повідомлення {contactName}", "@chat_sendMessageTo": { "placeholders": { @@ -466,17 +470,17 @@ "gifPicker_searchHint": "Пошук GIF...", "gifPicker_poweredBy": "На базі GIPHY", "gifPicker_noGifsFound": "GIF не знайдено", - "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", + "gifPicker_failedLoad": "Не вдалось завантажити GIF-файли", "gifPicker_failedSearch": "Пошук GIF не вдався", "gifPicker_noInternet": "Немає інтернет-з'єднання", - "debugLog_appTitle": "Журнал налагодження програми", + "debugLog_appTitle": "Журнал налагодження застосунку", "debugLog_bleTitle": "Журнал налагодження BLE", "debugLog_copyLog": "Копіювати журнал", "debugLog_clearLog": "Очистити журнал", "debugLog_copied": "Журнал налагодження скопійовано", "debugLog_bleCopied": "Журнал BLE скопійовано", "debugLog_noEntries": "Поки що немає записів журналу налагодження.", - "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", + "debugLog_enableInSettings": "Увімкніть налагодження застосунку в налаштуваннях", "debugLog_frames": "Кадри", "debugLog_rawLogRx": "Необроблений лог - RX", "debugLog_noBleActivity": "Поки що немає активності BLE.", @@ -546,12 +550,12 @@ "chat_pathManagement": "Керування шляхами", "chat_routingMode": "Режим маршрутизації", "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "chat_forceFloodMode": "Примусово на всю мережу", - "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", + "chat_forceFloodMode": "Примусово через всю мережу", + "chat_recentAckPaths": "Підтверджені шляхи (натисніть, щоб використати):", "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", - "chat_hopSingular": "Стрибок", - "chat_hopPlural": "стрибків", - "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", + "chat_hopSingular": "Перехід", + "chat_hopPlural": "переходів", + "chat_hopsCount": "{count} {count, plural, =1{перехід} few{переходи} many{переходів} other{переходів}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -560,6 +564,7 @@ } }, "chat_successes": "Успішно", + "chat_score": "Оцінка", "chat_removePath": "Видалити шлях", "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", "chat_pathActions": "Дії зі шляхом:", @@ -568,11 +573,11 @@ "chat_clearPath": "Очистити шлях", "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", - "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", - "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", + "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі застосунку", + "chat_floodModeEnabled": "Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", "chat_fullPath": "Повний шлях", "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", - "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", + "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{перехід} few{переходи} many{переходів} other{переходів}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -590,9 +595,9 @@ "chat_path": "Шлях", "chat_publicKey": "Відкритий ключ", "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", - "chat_floodForced": "На всю мережу (примусово)", - "chat_directForced": "Прямий (примусово)", - "chat_hopsForced": "{count} стрибків (примусово)", + "chat_floodForced": "Через всю мережу (примусово)", + "chat_directForced": "Напряму (примусово)", + "chat_hopsForced": "{count} переходів (примусово)", "@chat_hopsForced": { "placeholders": { "count": { @@ -600,9 +605,9 @@ } } }, - "chat_floodAuto": "На всю мережу (авто)", - "chat_direct": "Прямий", - "chat_poiShared": "Точкою інтересу поділилися", + "chat_floodAuto": "Через всю мережу (авто)", + "chat_direct": "Напряму", + "chat_poiShared": "Поділилися точкою інтересу", "chat_unread": "Непрочитано: {count}", "@chat_unread": { "placeholders": { @@ -614,7 +619,7 @@ "chat_openLink": "Відкрити посилання?", "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", "chat_open": "Відкрити", - "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", + "chat_couldNotOpenLink": "Не вдалось відкрити посилання: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -624,8 +629,8 @@ }, "chat_invalidLink": "Невірний формат посилання", "map_title": "Карта вузлів", - "map_noNodesWithLocation": "Немає вузлів з даними про розташування", - "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", + "map_noNodesWithLocation": "Немає вузлів з даними про геопозицію", + "map_nodesNeedGps": "Вузли мають надавати свої GPS координати,\nщоб з'явитись на карті.", "map_nodesCount": "Вузли: {count}", "@map_nodesCount": { "placeholders": { @@ -650,19 +655,25 @@ "map_pinPrivate": "Замок (Приватний)", "map_pinPublic": "Ключ (Публічний)", "map_lastSeen": "Останній раз бачили", - "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "map_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?", "map_from": "Від", "map_source": "Джерело", "map_flags": "Прапорці", - "map_shareMarkerHere": "Поділитися маркером тут", + "map_type": "Тип", + "map_path": "Шлях", + "map_location": "Геопозиція", + "map_estLocation": "Орієнтовна геопозиція", + "map_publicKey": "Публічний ключ", + "map_publicKeyPrefixHint": "напр. ab12", + "map_shareMarkerHere": "Поділитись маркером тут", "map_pinLabel": "Мітка піна", "map_label": "Мітка", "map_pointOfInterest": "Точка інтересу", "map_sendToContact": "Надіслати контакту", "map_sendToChannel": "Надіслати в канал", "map_noChannelsAvailable": "Немає доступних каналів", - "map_publicLocationShare": "Поділитися в публічному місці", - "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", + "map_publicLocationShare": "Поділитись в публічному місці", + "map_publicLocationShareConfirm": "Ви збираєтесь поділитись геопозицією у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -670,7 +681,7 @@ } } }, - "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", + "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитись маркерами", "map_filterNodes": "Фільтрувати вузли", "map_nodeTypes": "Типи вузлів", "map_chatNodes": "Вузли чату", @@ -683,7 +694,7 @@ "map_showSharedMarkers": "Показувати спільні маркери", "map_lastSeenTime": "Час останньої активності", "map_sharedPin": "Спільний пін", - "map_joinRoom": "Приєднатися до кімнати", + "map_joinRoom": "Приєднатись до кімнати", "map_manageRepeater": "Керувати ретранслятором", "mapCache_title": "Офлайн-кеш карти", "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", @@ -806,7 +817,7 @@ "time_minutes": "хвилин", "time_allTime": "Весь час", "dialog_disconnect": "Відключити", - "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?", "login_repeaterLogin": "Вхід у ретранслятор", "login_roomLogin": "Вхід у кімнату", "login_password": "Пароль", @@ -817,8 +828,8 @@ "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", "login_routing": "Маршрутизація", "login_routingMode": "Режим маршрутизації", - "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "login_forceFloodMode": "Примусово на всю мережу", + "login_autoUseSavedPath": "Авто (збережений шлях)", + "login_forceFloodMode": "Примусово через всю мережу", "login_managePaths": "Керувати шляхами", "login_login": "Вхід", "login_attempt": "Спроба {current}/{max}", @@ -851,7 +862,7 @@ } } }, - "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", + "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{переходом} few{переходами} many{переходами} other{переходами}}", "@path_usingHopsPath": { "placeholders": { "count": { @@ -861,10 +872,10 @@ }, "path_enterCustomPath": "Ввести власний шлях", "path_currentPathLabel": "Поточний шлях", - "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", + "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.", "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", "path_labelHexPrefixes": "Hex-префікси", - "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", + "path_helperMaxHops": "Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)", "path_selectFromContacts": "Вибрати з контактів:", "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", @@ -876,7 +887,7 @@ } } }, - "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", + "path_tooLong": "Шлях занадто довгий. Максимум 64 переходи.", "path_setPath": "Встановити шлях", "repeater_management": "Керування ретранслятором", "repeater_managementTools": "Інструменти керування", @@ -891,7 +902,7 @@ "repeater_statusTitle": "Статус ретранслятора", "repeater_routingMode": "Режим маршрутизації", "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "repeater_forceFloodMode": "Примусово на всю мережу", + "repeater_forceFloodMode": "Примусово через всю мережу", "repeater_pathManagement": "Керування шляхами", "repeater_refresh": "Оновити", "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", @@ -936,7 +947,7 @@ } } }, - "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetTxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -950,7 +961,7 @@ } } }, - "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetRxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -964,7 +975,7 @@ } } }, - "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", + "repeater_duplicatesFloodDirect": "Через всю мережу: {flood}, Прямі: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -999,7 +1010,7 @@ "repeater_bandwidth": "Смуга пропускання", "repeater_spreadingFactor": "Коефіцієнт розширення", "repeater_codingRate": "Швидкість кодування", - "repeater_locationSettings": "Налаштування розташування", + "repeater_locationSettings": "Налаштування геопозиції", "repeater_latitude": "Широта", "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", "repeater_longitude": "Довгота", @@ -1010,9 +1021,9 @@ "repeater_guestAccess": "Гостьовий доступ", "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", "repeater_privacyMode": "Режим приватності", - "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "repeater_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", "repeater_advertisementSettings": "Налаштування оголошень", - "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", + "repeater_localAdvertInterval": "Інтервал локальних оголошень (без ретрансляції)", "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1021,7 +1032,7 @@ } } }, - "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", + "repeater_floodAdvertInterval": "Інтервал оголошень через всю мережу (flood)", "repeater_floodAdvertIntervalHours": "{hours} годин", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1035,9 +1046,9 @@ "repeater_rebootRepeater": "Перезавантажити ретранслятор", "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", - "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", + "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентифікації", "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", - "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", + "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентифікацію для ретранслятора. Продовжити?", "repeater_eraseFileSystem": "Очистити файлову систему", "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", @@ -1146,6 +1157,7 @@ "repeater_refreshBasicSettings": "Оновити основні налаштування", "repeater_refreshRadioSettings": "Оновити налаштування радіо", "repeater_refreshTxPower": "Оновити потужність TX", + "repeater_refreshLocationSettings": "Оновити налаштування геопозиції", "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", "repeater_refreshGuestAccess": "Оновити гостьовий доступ", "repeater_refreshPrivacyMode": "Оновити режим приватності", @@ -1201,19 +1213,19 @@ "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", - "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", + "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість переходів для вхідних пакетів flood (якщо >= max, пакет не пересилається).", "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", - "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення через всю мережу. Встановіть 0 для вимкнення.", "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.", "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", - "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", + "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «через всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", @@ -1229,7 +1241,7 @@ "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", - "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", + "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «через всю мережу» (flood).", "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", @@ -1243,14 +1255,14 @@ "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.", - "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", - "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", + "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення геопозиції вузла:\n- none : не включати геопозицію в оголошення\n- share : ділитись геопозицією GPS (з SensorManager)\n- prefs : оголошувати геопозицію, збережену в налаштуваннях", + "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення геопозиції.", "repeater_commandsListTitle": "Список команд", "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", "repeater_general": "Загальні", "repeater_settingsCategory": "Налаштування", "repeater_bridge": "Міст", - "repeater_logging": "Логування", + "repeater_logging": "Журналювання", "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", @@ -1321,7 +1333,7 @@ "channelPath_title": "Шлях пакету", "channelPath_viewMap": "Показати карту", "channelPath_otherObservedPaths": "Інші спостережувані шляхи", - "channelPath_repeaterHops": "Стрибки ретранслятора", + "channelPath_repeaterHops": "Переходи через ретранслятори", "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", "channelPath_messageDetails": "Деталі повідомлення", "channelPath_senderLabel": "Відправник", @@ -1340,7 +1352,7 @@ } } }, - "channelPath_noLocationData": "Немає даних про розташування", + "channelPath_noLocationData": "Немає даних про геопозицію", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1364,9 +1376,9 @@ } }, "channelPath_unknownPath": "Невідомий", - "channelPath_floodPath": "На всю мережу", - "channelPath_directPath": "Прямий", - "channelPath_observedZeroOf": "0 з {total} стрибків", + "channelPath_floodPath": "Через всю мережу", + "channelPath_directPath": "Напряму", + "channelPath_observedZeroOf": "0 з {total} переходів", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1374,7 +1386,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} з {total} стрибків", + "channelPath_observedSomeOf": "{observed} з {total} переходів", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1415,12 +1427,12 @@ } } }, - "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", + "channelPath_noHopDetailsAvailable": "Деталі переходів недоступні для цього пакету.", "channelPath_unknownRepeater": "Невідомий ретранслятор", "listFilter_tooltip": "Фільтр та сортування", "listFilter_sortBy": "Сортувати за", "listFilter_latestMessages": "Останні повідомлення", - "listFilter_heardRecently": "Нещодавно чули", + "listFilter_heardRecently": "Нещодавно почуті", "listFilter_az": "А-Я", "listFilter_filters": "Фільтри", "listFilter_all": "Все", @@ -1437,20 +1449,20 @@ } }, "repeater_neighbors": "Сусіди", - "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", + "repeater_neighborsSubtitle": "Показати сусідів, доступних без ретрансляції.", "neighbors_receivedData": "Дані сусідів отримано", "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", "neighbors_noData": "Дані про сусідів недоступні.", "channels_createPrivateChannelDesc": "Захищено секретним ключем.", - "channels_joinPrivateChannel": "Приєднатися до приватного каналу", + "channels_joinPrivateChannel": "Приєднатись до приватного каналу", "channels_createPrivateChannel": "Створити приватний канал", "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", - "channels_joinPublicChannel": "Приєднатися до публічного каналу", - "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", - "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", - "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", + "channels_joinPublicChannel": "Приєднатись до публічного каналу", + "channels_joinPublicChannelDesc": "Будь-хто може приєднатись до цього каналу.", + "channels_joinHashtagChannel": "Приєднатись до хештег-каналу", + "channels_joinHashtagChannelDesc": "Будь-хто може приєднатись до хештег-каналів.", "channels_scanQrCode": "Сканувати QR-код", "channels_scanQrCodeComingSoon": "Скоро буде", "channels_enterHashtag": "Введіть хештег", @@ -1472,7 +1484,7 @@ "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", "neighbors_heardAgo": "Почуто: {time} тому", "settings_locationGPSEnable": "Увімкнути GPS", - "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", + "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення геопозиції через GPS.", "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.", "contacts_manageRoom": "Керувати сервером кімнати", @@ -1536,10 +1548,10 @@ "common_ok": "ОК", "community_title": "Спільнота", "community_create": "Створити спільноту", - "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", - "community_join": "Приєднатися", - "community_joinTitle": "Приєднатися до спільноти", - "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", + "community_createDesc": "Створити нову спільноту та поділитись через QR-код.", + "community_join": "Приєднатись", + "community_joinTitle": "Приєднатись до спільноти", + "community_joinConfirmation": "Ви бажаєте приєднатись до спільноти «{name}»?", "community_scanQr": "Сканувати QR спільноти", "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", "community_showQr": "Показати QR-код", @@ -1549,9 +1561,9 @@ "community_enterName": "Введіть назву спільноти", "community_created": "Спільноту «{name}» створено", "community_joined": "Приєднався до спільноти «{name}»", - "community_qrTitle": "Поділитися спільнотою", - "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", - "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", + "community_qrTitle": "Поділитись спільнотою", + "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатись до {name}", + "community_hashtagPrivacyHint": "Хештег-канали спільноти доступні лише членам спільноти", "community_invalidQrCode": "Недійсний QR-код спільноти", "community_alreadyMember": "Вже учасник", "community_alreadyMemberMessage": "Ви вже є учасником «{name}».", @@ -1572,10 +1584,10 @@ }, "community_deleted": "Спільноту «{name}» покинуто", "community_addHashtagChannel": "Додати хештег спільноти", - "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", + "community_addHashtagChannelDesc": "Додати хештег-канал для цієї спільноти", "community_selectCommunity": "Вибрати спільноту", "community_regularHashtag": "Звичайний хештег", - "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", + "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатись)", "community_communityHashtag": "Хештег спільноти", "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", "community_forCommunity": "Для {name}", @@ -1607,13 +1619,13 @@ } } }, - "community_regenerateSecret": "Перегенерувати секрет", - "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", + "community_regenerateSecret": "Перегенерувати секретний ключ", + "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Усі учасники матимуть відсканувати новий QR-код, щоб продовжити спілкування.", "community_regenerate": "Перегенерувати", - "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", - "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", - "community_updateSecret": "Оновити секрет", - "community_secretUpdated": "Зміну секрету для «{name}» оновлено", + "community_secretRegenerated": "Секретний ключ для «{name}» перегенеровано", + "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити секретний ключ для «{name}»", + "community_updateSecret": "Оновити секретний ключ", + "community_secretUpdated": "Секретний ключ для «{name}» оновлено", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1622,76 +1634,76 @@ } }, "pathTrace_you": "Ви", - "pathTrace_failed": "Відстеження шляху не вдалося.", + "pathTrace_failed": "Відстеження шляху не вдалось.", "pathTrace_notAvailable": "Трасування шляху недоступне.", - "pathTrace_refreshTooltip": "Оновити Path Trace", + "pathTrace_refreshTooltip": "Оновити трасування шляху", "contacts_pathTrace": "Трасування шляхів", "contacts_ping": "Пінгувати", - "contacts_repeaterPathTrace": "Трасування шляху до повторювача", - "contacts_repeaterPing": "Пінгувати повторювач", + "contacts_repeaterPathTrace": "Трасування шляху до ретранслятора", + "contacts_repeaterPing": "Пінгувати ретранслятор", "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", "contacts_roomPing": "Пінг сервера кімнати", "contacts_chatTraceRoute": "Трасування шляху", "contacts_pathTraceTo": "Відстежити маршрут до {name}", "contacts_invalidAdvertFormat": "Недійсні контактні дані", "contacts_contactImported": "Контакт було імпортовано.", - "contacts_contactImportFailed": "Контакт не вдалося імпортувати", - "contacts_zeroHopAdvert": "Реклама без перехоплення", - "contacts_floodAdvert": "Залив реклами", - "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", + "contacts_contactImportFailed": "Контакт не вдалось імпортувати", + "contacts_zeroHopAdvert": "Оголошення без ретрансляції", + "contacts_floodAdvert": "Оголошення з ретрансляцією", + "contacts_copyAdvertToClipboard": "Копіювати оголошення", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", - "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", - "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", - "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", + "contacts_zeroHopContactAdvertFailed": "Не вдалось надіслати контакт.", + "contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.", + "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилось невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", - "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "contacts_addContactFromClipboard": "Додати контакт з буфера", + "contacts_ShareContactZeroHop": "Поділитись контактом за оголошенням", "notification_activityTitle": "Активність MeshCore", "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", - "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", + "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нові вузли} many{нових вузлів} other{нових вузлів}}", "notification_newTypeDiscovered": "Виявлено новий {contactType}", "notification_receivedNewMessage": "Отримано нове повідомлення", - "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", - "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", + "settings_gpxExportRepeaters": "Експорт ретрансляторів і серверів кімнат у GPX", + "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори та сервери кімнат з геопозицією у файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", "settings_gpxExportNoContacts": "Немає контактів для експорту.", "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", - "settings_gpxExportError": "Сталася помилка під час експорту.", - "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", - "settings_gpxExportAll": "Експортувати всі контакти до GPX", - "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", - "settings_gpxExportContacts": "Експортувати супутників до GPX", - "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", - "settings_gpxExportChat": "Місця супутників", + "settings_gpxExportError": "Сталась помилка під час експорту.", + "settings_gpxExportAllSubtitle": "Експортує всі контакти з геопозицією у файл GPX.", + "settings_gpxExportAll": "Експорт усіх контактів у GPX", + "settings_gpxExportContactsSubtitle": "Експортує контакти з геопозицією у файл GPX.", + "settings_gpxExportContacts": "Експорт контактів у GPX", + "settings_gpxExportRepeatersRoom": "Геопозиції ретрансляторів та серверів кімнат", + "settings_gpxExportChat": "Геопозиції контактів", "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", "settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", - "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", + "pathTrace_someHopsNoLocation": "Один або декілька переходів не мають даних про геопозицію!", "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", "map_runTrace": "Виконати трасування шляху", "pathTrace_clearTooltip": "Очистити шлях", "map_removeLast": "Видалити останній", - "map_pathTraceCancelled": "Відмінується трасування шляху", + "map_pathTraceCancelled": "Трасування шляху скасовано.", "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", "scanner_chromeRequired": "Потрібен браузер Chrome", - "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", + "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому вебзастосунку потрібен Google Chrome або браузер на базі Chromium.", "scanner_bluetoothOff": "Bluetooth вимкнено", "snrIndicator_lastSeen": "Останній раз бачили", - "snrIndicator_nearByRepeaters": "Ближні ретранслятори", + "snrIndicator_nearByRepeaters": "Найближчі ретранслятори", "chat_ShowAllPaths": "Показати всі шляхи", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", "settings_clientRepeat": "Автономна система", "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "одиниці", - "appSettings_unitsMetric": "Метричний (м / км)", - "appSettings_unitsImperial": "Імперська (ft / mi)", + "appSettings_unitsTitle": "Одиниці", + "appSettings_unitsMetric": "Метричні (м / км)", + "appSettings_unitsImperial": "Імперські (ft / mi)", "map_lineOfSight": "Пряма видимість", "map_losScreenTitle": "Пряма видимість", "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", @@ -1709,7 +1721,7 @@ "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", "losShowDisplayNodes": "Показати вузли відображення", "losCustomPoints": "Користувальницькі точки", - "losCustomPointLabel": "Спеціальний {index}", + "losCustomPointLabel": "Власна точка {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1798,7 +1810,7 @@ }, "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", - "losRenameCustomPoint": "Перейменуйте спеціальну точку", + "losRenameCustomPoint": "Перейменувати власну точку", "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", @@ -1878,19 +1890,19 @@ "contacts_unread": "Непрочитане", "settings_contactSettingsSubtitle": "Налаштування для додавання контактів", "settings_contactSettings": "Налаштування контактів", - "contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів", - "contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі", - "contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори", + "contactsSettings_autoAddUsersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлених користувачів", + "contactsSettings_autoAddRepeatersTitle": "Автоматично додавати ретранслятори", + "contactsSettings_autoAddRepeatersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені ретранслятори", "contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат", "contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами", "contactsSettings_autoAddTitle": "Автоматичне виявлення", "contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів", "contactsSettings_title": "Налаштування контактів", - "contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.", - "contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики", + "contactsSettings_autoAddRoomServersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сервери кімнат.", + "contactsSettings_autoAddSensorsTitle": "Автоматично додавати сенсори", "discoveredContacts_searchHint": "Знайти виявлені контакти", "discoveredContacts_contactAdded": "Контакт додано", - "contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори", + "contactsSettings_autoAddSensorsSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сенсори", "contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше", "discoveredContacts_Title": "Виявлені контакти", "discoveredContacts_noMatching": "Відповідних контактів не знайдено", @@ -1901,9 +1913,9 @@ "common_deleteAll": "Видалити все", "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", - "map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", - "map_guessedLocation": "Визначено місцезнаходження", - "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", + "map_showGuessedLocations": "Показати геопозиції передбачених вузлів", + "map_guessedLocation": "Передбачена геопозиція", + "usbScreenSubtitle": "Виберіть виявлений USB-пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", "usbScreenTitle": "Підключити через USB", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", @@ -1915,8 +1927,8 @@ "usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.", "usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.", "usbErrorNotConnected": "Немає підключених пристроїв USB.", - "usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.", - "usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.", + "usbErrorOpenFailed": "Не вдалось відкрити вибране USB-пристрій.", + "usbErrorConnectFailed": "Не вдалось підключитись до вибраного USB-пристрою.", "usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.", "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", @@ -1931,9 +1943,9 @@ }, "usbStatus_searching": "Пошук пристроїв USB...", "usbStatus_notConnected": "Виберіть пристрій USB", - "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", + "usbConnectionFailed": "Не вдалось встановити з'єднання через USB: {error}", "usbStatus_connecting": "Підключення до USB-пристрою...", - "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", + "usbErrorConnectTimedOut": "З'єднання не вдалось встановити. Переконайтесь, що пристрій має встановлене програмне забезпечення USB Companion.", "@tcpStatus_connectingTo": { "placeholders": { "endpoint": { @@ -1951,18 +1963,18 @@ "connectionChoiceTcpLabel": "TCP", "tcpHostHint": "192.168.40.10", "tcpHostLabel": "IP-адреса", - "tcpScreenTitle": "З'єднатися через протокол TCP", + "tcpScreenTitle": "Підключитись через TCP", "tcpPortLabel": "Порт", "tcpPortHint": "5000", "tcpStatus_notConnected": "Введіть кінцеву точку та підключіться", "tcpStatus_connectingTo": "Підключення до {endpoint}...", "tcpErrorHostRequired": "Необхідно вказати IP-адресу.", - "tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.", + "tcpErrorPortInvalid": "Порт має бути в межах від 1 до 65535.", "tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.", - "tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", - "tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}", - "map_showDiscoveryContacts": "Показати контакти Відкриття", - "map_setAsMyLocation": "Встановити моє місцезнаходження", + "tcpErrorTimedOut": "З'єднання TCP завершилось через закінчення часу очікування.", + "tcpConnectionFailed": "Не вдалось встановити з'єднання TCP: {error}", + "map_showDiscoveryContacts": "Показати виявлені контакти", + "map_setAsMyLocation": "Встановити мою геопозицію", "@path_routeWeight": { "placeholders": { "weight": { @@ -1973,12 +1985,12 @@ } } }, - "settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися", + "settings_privacySubtitle": "Керуйте інформацією, яка буде спільно використовуватись", "settings_privacy": "Налаштування приватності", "settings_telemetryBaseMode": "Режим базової телеметрії", "settings_telemetryLocationMode": "Режим місця телеметрії", - "settings_advertLocation": "Розміщення реклами", - "settings_advertLocationSubtitle": "Включити місце розташування в оголошення", + "settings_advertLocation": "Геопозиція в оголошенні", + "settings_advertLocationSubtitle": "Включити геопозицію в оголошення", "settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.", "settings_allowAll": "Дозволити все", "settings_denyAll": "Відхилити все", @@ -1986,8 +1998,8 @@ "settings_telemetryEnvironmentMode": "Режим середовища телеметрії", "contact_info": "Контактна інформація", "contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії", - "contact_teleLoc": "Розташування телеметрії", - "contact_teleBase": "Базовий телебачення", + "contact_teleLoc": "Геопозиція телеметрії", + "contact_teleBase": "Базова телеметрія", "contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення", "contact_settings": "Налаштування контактів", "contact_telemetry": "Телеметрія", @@ -1995,13 +2007,6 @@ "contact_lastSeen": "Останній раз бачили", "contact_teleEnv": "Середовище телеметрії", "contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_initialRouteWeight": "Початкова вартість маршруту", "appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів", "appSettings_maxRouteWeight": "Максимальна вага маршруту", @@ -2014,9 +2019,8 @@ "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", "path_routeWeight": "{weight}/{max}", "settings_telemetryModeUpdated": "Режим телеметрії оновлено", - "settings_multiAck": "Багатократне підтвердження: {value}", - "map_showOverlaps": "Перекриття ключа повторювача", - "map_runTraceWithReturnPath": "Повернутися назад тим же шляхом", + "map_showOverlaps": "Перекриття ключів ретрансляторів", + "map_runTraceWithReturnPath": "Повернутись назад тим же шляхом", "@radioStats_noiseFloor": { "placeholders": { "noiseDbm": { @@ -2060,26 +2064,26 @@ } }, "chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.", - "appSettings_languageHu": "Угорський", - "appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.", + "appSettings_languageHu": "Угорська", + "appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.", "appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення", "appSettings_languageJa": "Японська", - "appSettings_languageKo": "Кореєська", + "appSettings_languageKo": "Корейська", "radioStats_tooltip": "Статистика радіо та мережі", - "radioStats_screenTitle": "Дані про радіостанції", - "radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.", - "radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.", - "radioStats_waiting": "Очікую на отримання даних…", + "radioStats_screenTitle": "Статистика радіо", + "radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіо.", + "radioStats_firmwareTooOld": "Статистика радіо вимагає прошивки пристрою-компаньйона версії 8 або новішої.", + "radioStats_waiting": "Очікування даних…", "radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм", "radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм", "radioStats_lastSnr": "Останній показник SNR: {snr} дБ", - "radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд", - "radioStats_rxAir": "Загальний час використання RX: {seconds} секунд", + "radioStats_txAir": "Час в ефірі TX (загальний): {seconds} секунд", + "radioStats_rxAir": "Час в ефірі RX (загальний): {seconds} секунд", "radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.", "radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм", "radioStats_stripWaiting": "Отримано статистику радіо…", - "radioStats_settingsTile": "Дані про радіостанції", - "radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.", + "radioStats_settingsTile": "Статистика радіо", + "radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час в ефірі.", "@translation_downloadFailed": { "placeholders": { "error": { @@ -2093,9 +2097,9 @@ "translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.", "translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.", "translation_targetLanguage": "Цільова мова", - "translation_useAppLanguage": "Використовуйте мову додатку", + "translation_useAppLanguage": "Використовувати мову застосунку", "translation_downloadedModelLabel": "Завантажений шаблон", - "translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face", + "translation_presetModelLabel": "Попередньо налаштована модель з Hugging Face", "translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією", "translation_downloadModel": "Завантажити модель", "translation_downloading": "Завантаження...", @@ -2106,7 +2110,7 @@ "translation_deleteModel": "Видалити модель", "translation_modelDownloaded": "Модель перекладу завантажена.", "translation_downloadStopped": "Завантаження призупинено.", - "translation_downloadFailed": "Не вдалося завантажити: {error}", + "translation_downloadFailed": "Не вдалось завантажити: {error}", "translation_enterUrlFirst": "Спочатку введіть URL моделі.", "@scanner_linuxPairingPinPrompt": { "placeholders": { @@ -2134,7 +2138,7 @@ "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingHidePin": "Приховати PIN", "repeater_cliQuickClockSync": "Синхронізація годинника", - "repeater_cliQuickDiscovery": "Відкрити сусідів", + "repeater_cliQuickDiscovery": "Виявити сусідів", "@repeater_clockSyncAfterLogin": { "description": "Repeater setting: auto sync device clock after successful login" }, @@ -2143,7 +2147,7 @@ }, "repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.", "repeater_clockSyncAfterLogin": "Синхронізація годин після входу", - "repeater_guestTools": "Інструменти для гостей", + "repeater_guestTools": "Гостьові інструменти", "repeater_guest": "Інформація про ретранслятор", "room_guest": "Інформація про сервер кімнати", "chat_sendMessage": "Надіслати повідомлення", @@ -2217,5 +2221,57 @@ "repeater_cliHelpRegionListDenied": "Перелік регіонів, які забороняють рух транспорту під час повені.", "repeater_cliHelpStatsPackets": "(Тільки для серійного використання) Відображає статистику на рівні пакетів.", "repeater_cliHelpStatsRadio": "(Тільки для серій) Відображає радіостатистику.", - "repeater_cliHelpStatsCore": "(Тільки для серійного використання) Відображає основні статистичні дані про програмне забезпечення." + "repeater_cliHelpStatsCore": "(Тільки для серійного використання) Відображає основні статистичні дані про програмне забезпечення.", + "background_serviceTitle": "MeshCore працює", + "background_serviceText": "Підтримує з'єднання BLE", + "appSettings_translationModelDeleted": "Видалено {name}", + "appSettings_translationModelDeleteFailed": "Не вдалось видалити: {error}", + "channels_channelUpdateFailed": "Не вдалось оновити канал: {error}", + "contact_typeChat": "Чат", + "contact_typeRepeater": "Ретранслятор", + "contact_typeRoom": "Кімната", + "contact_typeSensor": "Сенсор", + "contact_typeUnknown": "Невідомо", + "settings_multiAck": "Багато підтверджень", + "map_sharedAt": "Поділено", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsHint": "Натисніть на заблоковане місце, щоб виділити його на карті.", + "losBlockedSpotsTitle": "Заблоковані місця", + "losSelectedObstructionTitle": "Вибраний об'єкт перешкоди", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d942b4fe..816d3ff0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -109,6 +109,8 @@ "settings_privacyModeEnabled": "隐私模式已启用", "settings_privacyModeDisabled": "隐私模式已关闭", "settings_actions": "操作", + "settings_deleteAllPaths": "Delete All Paths", + "settings_deleteAllPathsSubtitle": "Clear all path data from contacts.", "settings_sendAdvertisement": "发送广播", "settings_sendAdvertisementSubtitle": "立即发送广播", "settings_advertisementSent": "已发送广播", @@ -2000,13 +2002,6 @@ "contact_settings": "联系人设置", "contact_teleLocSubtitle": "允许共享位置数据", "contact_telemetry": "遥测数据", - "@settings_multiAck": { - "placeholders": { - "value": { - "type": "String" - } - } - }, "appSettings_maxRouteWeight": "最大路径重量", "appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量", "appSettings_initialRouteWeight": "初始路线权重", @@ -2018,7 +2013,7 @@ "appSettings_maxMessageRetries": "最大消息重试次数", "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", "path_routeWeight": "{weight}/{max}", - "settings_multiAck": "多重ACK:{value}", + "settings_multiAck": "多重ACK", "settings_telemetryModeUpdated": "遥测模式已更新", "map_showOverlaps": "重复键重叠", "map_runTraceWithReturnPath": "沿着相同的路径返回", @@ -2222,5 +2217,86 @@ "repeater_cliHelpRegionListDenied": "列出禁止洪水交通的区域。", "repeater_cliHelpStatsPackets": "(仅显示序列信息)显示数据包级别的统计信息。", "repeater_cliHelpStatsRadio": "(仅显示序列信息)显示收音机相关统计数据。", - "repeater_cliHelpStatsCore": "(仅显示序列号)显示核心固件统计信息。" + "repeater_cliHelpStatsCore": "(仅显示序列号)显示核心固件统计信息。", + "common_done": "Done", + "background_serviceTitle": "MeshCore running", + "background_serviceText": "Keeping BLE connected", + "appSettings_translationModelDeleted": "Deleted {name}", + "@appSettings_translationModelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "appSettings_translationModelDeleteFailed": "Failed to delete: {error}", + "@appSettings_translationModelDeleteFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "channels_channelUpdateFailed": "Failed to update channel: {error}", + "@channels_channelUpdateFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "map_type": "Type", + "map_path": "Path", + "map_location": "Location", + "map_estLocation": "Est. Location", + "map_publicKey": "Public Key", + "map_publicKeyPrefixHint": "e.g. ab12", + "contact_typeChat": "Chat", + "contact_typeRepeater": "Repeater", + "contact_typeRoom": "Room", + "contact_typeSensor": "Sensor", + "contact_typeUnknown": "Unknown", + "channels_via": "via {path}", + "chat_score": "Score", + "map_sharedAt": "已分享", + "@losBlockedSpotChip": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "@losSelectedObstructionDetails": { + "placeholders": { + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + }, + "distanceFromA": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "distanceFromB": { + "type": "String" + } + } + }, + "losBlockedSpotsTitle": "被占用区域", + "losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。", + "losSelectedObstructionTitle": "选择性阻碍", + "losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}", + "losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})." } diff --git a/lib/l10n/contact_localization.dart b/lib/l10n/contact_localization.dart new file mode 100644 index 00000000..d8344a32 --- /dev/null +++ b/lib/l10n/contact_localization.dart @@ -0,0 +1,36 @@ +import '../connector/meshcore_protocol.dart'; +import '../models/contact.dart'; +import 'app_localizations.dart'; + +/// UI-level localization helpers for [Contact]. +/// +/// Kept out of the model layer so `Contact` does not depend on +/// `AppLocalizations`. Use these from widgets/screens; for logs and +/// non-UI export use `Contact.typeLabelRaw`. +extension ContactLocalization on Contact { + String typeLabel(AppLocalizations l10n) { + switch (type) { + case advTypeChat: + return l10n.contact_typeChat; + case advTypeRepeater: + return l10n.contact_typeRepeater; + case advTypeRoom: + return l10n.contact_typeRoom; + case advTypeSensor: + return l10n.contact_typeSensor; + default: + return l10n.contact_typeUnknown; + } + } + + String pathLabel(AppLocalizations l10n) { + if (pathOverride != null) { + if (pathOverride! < 0) return l10n.chat_floodForced; + if (pathOverride == 0) return l10n.chat_directForced; + return l10n.chat_hopsForced(pathOverride!); + } + if (pathLength < 0) return l10n.channelPath_floodPath; + if (pathLength == 0) return l10n.chat_direct; + return l10n.chat_hopsCount(pathLength); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3e57eb10..cd622811 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,6 +59,9 @@ void main() async { final notificationService = NotificationService(); await notificationService.initialize(); await backgroundService.initialize(); + backgroundService.setLanguageOverrideProvider( + () => appSettingsService.settings.languageOverride, + ); _registerThirdPartyLicenses(); await chatTextScaleService.initialize(); diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 2699f939..01d10a9c 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -17,6 +17,7 @@ class Contact { final double? longitude; final DateTime lastSeen; final DateTime lastMessageAt; + final DateTime? lastModified; final bool isActive; final bool wasPulled; final Uint8List? rawPacket; @@ -33,6 +34,7 @@ class Contact { this.latitude, this.longitude, required this.lastSeen, + this.lastModified, DateTime? lastMessageAt, this.isActive = true, this.wasPulled = false, @@ -41,7 +43,10 @@ class Contact { String get publicKeyHex => pubKeyToHex(publicKey); - String get typeLabel { + /// Non-localized type label, intended for logs and non-UI exports + /// (e.g. GPX). For UI use the `typeLabel(l10n)` extension in + /// `lib/l10n/contact_localization.dart`. + String get typeLabelRaw { switch (type) { case advTypeChat: return 'Chat'; @@ -56,17 +61,6 @@ class Contact { } } - String get pathLabel { - if (pathOverride != null) { - if (pathOverride! < 0) return 'Flood (forced)'; - if (pathOverride == 0) return 'Direct (forced)'; - return '$pathOverride hops (forced)'; - } - if (pathLength < 0) return 'Flood'; - if (pathLength == 0) return 'Direct'; - return '$pathLength hops'; - } - bool get hasLocation { const double epsilon = 1e-6; final lat = latitude ?? 0.0; @@ -94,6 +88,7 @@ class Contact { double? longitude, DateTime? lastSeen, DateTime? lastMessageAt, + DateTime? lastModified, bool? isActive, Uint8List? rawPacket, }) { @@ -114,6 +109,7 @@ class Contact { longitude: longitude ?? this.longitude, lastSeen: lastSeen ?? this.lastSeen, lastMessageAt: lastMessageAt ?? this.lastMessageAt, + lastModified: lastModified ?? this.lastModified, isActive: isActive ?? this.isActive, rawPacket: rawPacket ?? this.rawPacket, ); @@ -182,16 +178,34 @@ class Contact { return null; } - final lastMod = reader.readUInt32LE(); + // mandatory last_advert_timestamp + final lastAdvertTimestamp = reader.readUInt32LE(); double? lat, lon; - if (reader.remaining >= 8) { + DateTime? lastModified; + if (reader.remaining >= 12) { + final latRaw = reader.readInt32LE(); + final lonRaw = reader.readInt32LE(); + final lastModRaw = reader.readUInt32LE(); + // TODO: should this be &&? + if (latRaw != 0 || lonRaw != 0) { + lat = latRaw / 1e6; + lon = lonRaw / 1e6; + } + if (lastModRaw != 0) { + lastModified = DateTime.fromMillisecondsSinceEpoch(lastModRaw * 1000); + } + } else if (reader.remaining >= 8) { + // Old layout: gps without lastmod final latRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE(); if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; } + appLogger.info( + 'Contact ${pubKeyToHex(pubKey).substring(0, 8)} has gps but no lastmod (legacy firmware layout)', + ); } return Contact( @@ -203,7 +217,10 @@ class Contact { path: pathBytes, latitude: lat, longitude: lon, - lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), + lastSeen: DateTime.fromMillisecondsSinceEpoch( + lastAdvertTimestamp * 1000, + ), + lastModified: lastModified, isActive: true, rawPacket: null, ); diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 37ef3ccb..099d9201 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -228,7 +228,7 @@ class RadioSettings { frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 20, ), ), @@ -238,7 +238,7 @@ class RadioSettings { frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 14, ), ), @@ -248,7 +248,7 @@ class RadioSettings { frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 20, ), ), diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 80d8adbc..5d81e1ed 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -1237,15 +1237,20 @@ class AppSettingsScreen extends StatelessWidget { if (!context.mounted) return; showDismissibleSnackBar( context, - // TODO: l10n - content: Text('Deleted ${translationModelFriendlyName(model)}.'), + content: Text( + context.l10n.appSettings_translationModelDeleted( + translationModelFriendlyName(model), + ), + ), ); } catch (error) { if (!context.mounted) return; showDismissibleSnackBar( context, - content: Text('Delete failed: $error'), - ); // TODO: l10n + content: Text( + context.l10n.appSettings_translationModelDeleteFailed('$error'), + ), + ); } } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index e5b5f67e..bfcc301b 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,7 +4,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -13,7 +13,6 @@ import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; -import '../helpers/utf8_length_limiter.dart'; import '../helpers/snack_bar_builder.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; @@ -23,6 +22,7 @@ import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; +import '../widgets/byte_count_input.dart'; import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -32,13 +32,19 @@ import '../widgets/message_translation_button.dart'; import '../widgets/message_status_icon.dart'; import '../widgets/radio_stats_entry.dart'; import '../widgets/translated_message_content.dart'; +import '../widgets/unread_divider.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; class ChannelChatScreen extends StatefulWidget { final Channel channel; + final int initialUnreadCount; - const ChannelChatScreen({super.key, required this.channel}); + const ChannelChatScreen({ + super.key, + required this.channel, + this.initialUnreadCount = 0, + }); @override State createState() => _ChannelChatScreenState(); @@ -55,32 +61,46 @@ class _ChannelChatScreenState extends State { MeshCoreConnector? _connector; DateTime? _lastChannelSendAt; bool _channelSkipNextBottomSnap = false; + String? _unreadDividerMessageId; + + String? _cachedFormatLocale; + late DateFormat _hmFormat; + late DateFormat _mdFormat; @override void initState() { super.initState(); _textFieldFocusNode.addListener(_onTextFieldFocusChange); _scrollController.onScrollNearTop = _loadOlderMessages; + _scrollController.showJumpToBottom.addListener(_clearDividerAtBottom); SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final connector = context.read(); final settings = context.read().settings; final idx = widget.channel.index; - final unread = connector.getUnreadCountForChannelIndex(idx); + final unread = widget.initialUnreadCount; + final messages = connector.getChannelMessages(widget.channel); ChannelMessage? anchor; - if (settings.jumpToOldestUnread && unread > 0) { - anchor = _findOldestUnreadChannelAnchor( - connector.getChannelMessages(widget.channel), - unread, - ); + if (unread > 0) { + anchor = _findOldestUnreadChannelAnchor(messages, unread); } + setState(() { + if (anchor != null) _unreadDividerMessageId = anchor.messageId; + }); connector.setActiveChannel(idx); _connector = connector; - if (anchor != null) { + if (anchor != null && settings.jumpToOldestUnread) { _channelSkipNextBottomSnap = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - _scrollToMessage(anchor!.messageId); + _scrollController.jumpToEstimatedOffset( + unreadCount: unread, + totalMessages: messages.length, + onJumped: () { + if (!mounted) return; + _scrollToMessage(anchor!.messageId); + }, + ); }); } }); @@ -102,6 +122,13 @@ class _ChannelChatScreenState extends State { return oldest; } + void _clearDividerAtBottom() { + if (!_scrollController.showJumpToBottom.value && + _unreadDividerMessageId != null) { + setState(() => _unreadDividerMessageId = null); + } + } + void _onTextFieldFocusChange() { if (_textFieldFocusNode.hasFocus && mounted) { _scrollController.handleKeyboardOpen(); @@ -123,6 +150,7 @@ class _ChannelChatScreenState extends State { @override void dispose() { _connector?.setActiveChannel(null); + _scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom); _textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.dispose(); _textController.dispose(); @@ -321,6 +349,9 @@ class _ChannelChatScreenState extends State { if (!_messageKeys.containsKey(message.messageId)) { _messageKeys[message.messageId] = GlobalKey(); } + final isUnreadAnchor = + _unreadDividerMessageId != null && + message.messageId == _unreadDividerMessageId; return Container( key: _messageKeys[message.messageId]!, child: Builder( @@ -329,10 +360,17 @@ class _ChannelChatScreenState extends State { .select( (service) => service.scale, ); - return _buildMessageBubble( + final bubble = _buildMessageBubble( message, textScale, ); + if (isUnreadAnchor) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [const UnreadDivider(), bubble], + ); + } + return bubble; }, ), ); @@ -352,12 +390,24 @@ class _ChannelChatScreenState extends State { ); } + void _markAsUnread(ChannelMessage message) { + final connector = context.read(); + final messages = connector.getChannelMessages(widget.channel); + var count = 0; + var found = false; + for (final m in messages) { + if (m.messageId == message.messageId) found = true; + if (found && !m.isOutgoing) count++; + } + connector.setChannelUnreadCount(widget.channel.index, count); + } + Widget _buildMessageBubble(ChannelMessage message, double textScale) { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final gifId = GifHelper.parseGif(message.text); - final poi = _parsePoiMessage(message.text); + final poi = parseMarkerText(message.text); final translatedDisplayText = message.translatedText != null && message.translatedText!.trim().isNotEmpty @@ -445,6 +495,7 @@ class _ChannelChatScreenState extends State { poi, isOutgoing, textScale, + message.senderName, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -555,7 +606,9 @@ class _ChannelChatScreenState extends State { ? const EdgeInsets.symmetric(horizontal: 8) : EdgeInsets.zero, child: Text( - 'via ${_formatPathPrefixes(displayPath)}', + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), style: TextStyle( fontSize: 11, color: Colors.grey[600], @@ -576,7 +629,7 @@ class _ChannelChatScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - _formatTime(message.timestamp), + _formatTime(context, message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], @@ -701,7 +754,7 @@ class _ChannelChatScreenState extends State { final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); final gifId = GifHelper.parseGif(replyText); - final poi = _parsePoiMessage(replyText); + final poi = parseMarkerText(replyText); Widget contentPreview; if (gifId != null) { @@ -812,24 +865,12 @@ class _ChannelChatScreenState extends State { ); } - _PoiInfo? _parsePoiMessage(String text) { - final trimmed = text.trim(); - final match = RegExp( - r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|', - ).firstMatch(trimmed); - if (match == null) return null; - final lat = double.tryParse(match.group(1) ?? ''); - final lon = double.tryParse(match.group(2) ?? ''); - if (lat == null || lon == null) return null; - final label = match.group(3) ?? ''; - return _PoiInfo(lat: lat, lon: lon, label: label); - } - Widget _buildPoiMessage( BuildContext context, - _PoiInfo poi, + MarkerPayload poi, bool isOutgoing, - double textScale, { + double textScale, + String senderName, { Widget? trailing, }) { final colorScheme = Theme.of(context).colorScheme; @@ -849,12 +890,22 @@ class _ChannelChatScreenState extends State { padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () { + final selfName = context.read().selfName ?? 'Me'; + final fromName = isOutgoing ? selfName : senderName; + final key = buildSharedMarkerKey( + sourceId: 'channel:${widget.channel.index}', + label: poi.label, + fromName: fromName, + flags: poi.flags, + isChannel: true, + ); Navigator.push( context, MaterialPageRoute( builder: (context) => MapScreen( - highlightPosition: LatLng(poi.lat, poi.lon), + highlightPosition: poi.position, highlightLabel: poi.label, + highlightMarkerKey: key, ), ), ); @@ -1093,27 +1144,33 @@ class _ChannelChatScreenState extends State { ), ); } - - return TextField( + return ByteCountedTextField( + maxBytes: maxBytes, controller: _textController, focusNode: _textFieldFocusNode, - inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), - ], - textCapitalization: TextCapitalization.sentences, + hintText: context.l10n.chat_typeMessage, + onSubmitted: (_) => _sendMessage(), + encoder: + connector.isChannelSmazEnabled(widget.channel.index) + ? (text) => connector.prepareChannelOutboundText( + widget.channel.index, + text, + ) + : null, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), ), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerLow, contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + horizontal: 20, + vertical: 14, ), ), - maxLines: null, - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(), ); }, ), @@ -1195,7 +1252,11 @@ class _ChannelChatScreenState extends State { } final maxBytes = maxChannelMessageBytes(connector.selfName); - if (utf8.encode(messageText).length > maxBytes) { + final outboundText = connector.prepareChannelOutboundText( + widget.channel.index, + messageText, + ); + if (utf8.encode(outboundText).length > maxBytes) { showDismissibleSnackBar( context, content: Text(context.l10n.chat_messageTooLong(maxBytes)), @@ -1215,14 +1276,21 @@ class _ChannelChatScreenState extends State { ); } - String _formatTime(DateTime time) { + String _formatTime(BuildContext context, DateTime time) { final now = DateTime.now(); final diff = now.difference(time); + final locale = Localizations.localeOf(context).toString(); + if (locale != _cachedFormatLocale) { + _cachedFormatLocale = locale; + _hmFormat = DateFormat.Hm(locale); + _mdFormat = DateFormat.Md(locale); + } + final hm = _hmFormat.format(time); if (diff.inDays > 0) { - return '${time.day}/${time.month} ${time.hour}:${time.minute.toString().padLeft(2, '0')}'; + return '${_mdFormat.format(time)} $hm'; } else { - return '${time.hour}:${time.minute.toString().padLeft(2, '0')}'; + return hm; } } @@ -1278,6 +1346,15 @@ class _ChannelChatScreenState extends State { _copyMessageText(message.text); }, ), + if (!message.isOutgoing) + ListTile( + leading: const Icon(Icons.mark_chat_unread_outlined), + title: Text(context.l10n.chat_markAsUnread), + onTap: () { + Navigator.pop(sheetContext); + _markAsUnread(message); + }, + ), ListTile( leading: const Icon(Icons.delete_outline), title: Text(context.l10n.common_delete), @@ -1497,11 +1574,3 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { ); } } - -class _PoiInfo { - final double lat; - final double lon; - final String label; - - const _PoiInfo({required this.lat, required this.lon, required this.label}); -} diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 53769d40..afb2b906 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -304,6 +304,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget { class _ChannelMessagePathMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + static const double _mapMinZoom = 2.0; + static const double _mapMaxZoom = 18.0; final MapController _mapController = MapController(); Uint8List? _selectedPath; @@ -330,6 +332,18 @@ class _ChannelMessagePathMapScreenState } } + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } + + bool _isDesktopPlatform(TargetPlatform platform) { + return platform == TargetPlatform.linux || + platform == TargetPlatform.windows || + platform == TargetPlatform.macOS; + } + double _getPathDistance(List points) { double totalDistance = 0.0; final distanceCalculator = Distance(); @@ -357,6 +371,70 @@ class _ChannelMessagePathMapScreenState }); } + void _zoomMapBy(double delta) { + final camera = _mapController.camera; + final nextZoom = (camera.zoom + delta) + .clamp(_mapMinZoom, _mapMaxZoom) + .toDouble(); + _mapController.move(camera.center, nextZoom); + } + + void _resetMapView({ + required LatLng initialCenter, + required double initialZoom, + required LatLngBounds? bounds, + }) { + if (bounds != null) { + _mapController.fitCamera( + CameraFit.bounds( + bounds: bounds, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + ); + return; + } + _mapController.move(initialCenter, initialZoom); + } + + Widget _buildDesktopMapControls({ + required LatLng initialCenter, + required double initialZoom, + required LatLngBounds? bounds, + }) { + return Positioned( + left: 16, + top: 16, + child: Card( + elevation: 4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: 'Zoom in', + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: 'Zoom out', + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: 'Center map', + onPressed: () => _resetMapView( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Consumer( @@ -372,6 +450,7 @@ class _ChannelMessagePathMapScreenState primaryPath, widget.message.pathVariants, ); + final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final selectedPathTmp = _resolveSelectedPath( _selectedPath, observedPaths, @@ -451,10 +530,20 @@ class _ChannelMessagePathMapScreenState padding: const EdgeInsets.all(64), maxZoom: 16, ), - minZoom: 2.0, - maxZoom: 18.0, + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, interactionOptions: InteractionOptions( flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), ), onPositionChanged: (camera, hasGesture) { final shouldShow = camera.zoom >= _labelZoomThreshold; @@ -486,6 +575,12 @@ class _ChannelMessagePathMapScreenState ), ], ), + if (isDesktop) + _buildDesktopMapControls( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + ), if (observedPaths.length > 1) _buildPathSelector(context, observedPaths, selectedIndex, ( index, diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 44c7a69c..5db12852 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -492,13 +492,19 @@ class _ChannelsScreenState extends State ], ), onTap: () async { + final unread = connector.getUnreadCountForChannelIndex( + channel.index, + ); connector.markChannelRead(channel.index); await Future.delayed(const Duration(milliseconds: 50)); if (context.mounted) { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChannelChatScreen(channel: channel), + builder: (context) => ChannelChatScreen( + channel: channel, + initialUnreadCount: unread, + ), ), ); } @@ -1492,7 +1498,9 @@ class _ChannelsScreenState extends State if (!context.mounted) return; showDismissibleSnackBar( context, - content: Text('Failed to update channel: $e'), + content: Text( + context.l10n.channels_channelUpdateFailed('$e'), + ), ); } }, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 2aee61c3..54e1ecbe 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -9,7 +9,6 @@ import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../utils/platform_info.dart'; -import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; @@ -18,9 +17,9 @@ import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/gif_helper.dart'; import '../helpers/path_helper.dart'; -import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../models/message.dart'; import '../models/path_history.dart'; import '../models/translation_support.dart'; @@ -30,6 +29,7 @@ import '../services/path_history_service.dart'; import '../services/translation_service.dart'; import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/elements_ui.dart'; +import '../widgets/byte_count_input.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; import '../utils/emoji_utils.dart'; @@ -44,12 +44,18 @@ import '../widgets/translated_message_content.dart'; import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; import '../helpers/snack_bar_builder.dart'; +import '../widgets/unread_divider.dart'; import 'telemetry_screen.dart'; class ChatScreen extends StatefulWidget { final Contact contact; + final int initialUnreadCount; - const ChatScreen({super.key, required this.contact}); + const ChatScreen({ + super.key, + required this.contact, + this.initialUnreadCount = 0, + }); @override State createState() => _ChatScreenState(); @@ -63,6 +69,7 @@ class _ChatScreenState extends State { bool _isLoadingOlder = false; MeshCoreConnector? _connector; Message? _pendingUnreadScrollTarget; + String? _unreadDividerMessageId; DateTime? _lastTextSendAt; @override @@ -70,34 +77,47 @@ class _ChatScreenState extends State { super.initState(); _textFieldFocusNode.addListener(_onTextFieldFocusChange); _scrollController.onScrollNearTop = _loadOlderMessages; + _scrollController.showJumpToBottom.addListener(_clearDividerAtBottom); SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final connector = context.read(); final settings = context.read().settings; final keyHex = widget.contact.publicKeyHex; - final unread = connector.getUnreadCountForContactKey(keyHex); + final unread = widget.initialUnreadCount; + final messages = connector.getMessages(widget.contact); Message? anchor; - if (settings.jumpToOldestUnread && unread > 0) { - anchor = _findOldestUnreadAnchor( - connector.getMessages(widget.contact), - unread, - ); + if (unread > 0) { + anchor = _findOldestUnreadAnchor(messages, unread); } + setState(() { + if (anchor != null) _unreadDividerMessageId = anchor.messageId; + if (anchor != null && settings.jumpToOldestUnread) { + _pendingUnreadScrollTarget = anchor; + } + }); connector.setActiveContact(keyHex); _connector = connector; - if (anchor != null) { - setState(() => _pendingUnreadScrollTarget = anchor); + if (anchor != null && settings.jumpToOldestUnread) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final ctx = _unreadScrollKey.currentContext; - if (ctx != null) { - Scrollable.ensureVisible( - ctx, - duration: const Duration(milliseconds: 350), - alignment: 0.15, - ); - } - setState(() => _pendingUnreadScrollTarget = null); + _scrollController.jumpToEstimatedOffset( + unreadCount: unread, + totalMessages: messages.length, + onJumped: () async { + if (!mounted) return; + final ctx = _unreadScrollKey.currentContext; + if (ctx != null) { + await Scrollable.ensureVisible( + ctx, + duration: const Duration(milliseconds: 350), + alignment: 0.15, + ); + } + if (mounted) { + setState(() => _pendingUnreadScrollTarget = null); + } + }, + ); }); } }); @@ -116,6 +136,13 @@ class _ChatScreenState extends State { return oldest; } + void _clearDividerAtBottom() { + if (!_scrollController.showJumpToBottom.value && + _unreadDividerMessageId != null) { + setState(() => _unreadDividerMessageId = null); + } + } + void _onTextFieldFocusChange() { if (_textFieldFocusNode.hasFocus && mounted) { _scrollController.handleKeyboardOpen(); @@ -137,6 +164,7 @@ class _ChatScreenState extends State { @override void dispose() { _connector?.setActiveContact(null); + _scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom); _textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.dispose(); _textController.dispose(); @@ -479,6 +507,7 @@ class _ChatScreenState extends State { senderName: resolvedContact.type == advTypeRoom ? "${contact.name} [$fourByteHex]" : contact.name, + sourceId: widget.contact.publicKeyHex, isRoomServer: resolvedContact.type == advTypeRoom, textScale: textScale, onTap: () => _openMessagePath(message, contact), @@ -486,10 +515,19 @@ class _ChatScreenState extends State { onRetryReaction: (msg, emoji) => _sendReaction(msg, contact, emoji), ); + final isUnreadAnchor = + _unreadDividerMessageId != null && + message.messageId == _unreadDividerMessageId; + final child = isUnreadAnchor + ? Column( + mainAxisSize: MainAxisSize.min, + children: [const UnreadDivider(), bubble], + ) + : bubble; if (identical(message, _pendingUnreadScrollTarget)) { - return KeyedSubtree(key: _unreadScrollKey, child: bubble); + return KeyedSubtree(key: _unreadScrollKey, child: child); } - return bubble; + return child; }, ); }, @@ -497,6 +535,18 @@ class _ChatScreenState extends State { ); } + void _markAsUnread(Message message) { + final connector = context.read(); + final messages = connector.getMessages(widget.contact); + var count = 0; + var found = false; + for (final m in messages) { + if (m.messageId == message.messageId) found = true; + if (found && !m.isOutgoing && !m.isCli) count++; + } + connector.setContactUnreadCount(widget.contact.publicKeyHex, count); + } + Widget _buildInputBar(MeshCoreConnector connector) { final maxBytes = maxContactMessageBytes(); final colorScheme = Theme.of(context).colorScheme; @@ -567,24 +617,35 @@ class _ChatScreenState extends State { ), ); } - - return TextField( + return ByteCountedTextField( + maxBytes: maxBytes, controller: _textController, focusNode: _textFieldFocusNode, - inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), - ], - textCapitalization: TextCapitalization.sentences, + hintText: context.l10n.chat_typeMessage, + onSubmitted: (_) => _sendMessage(connector), + encoder: + connector.isContactSmazEnabled( + widget.contact.publicKeyHex, + ) + ? (text) => connector.prepareContactOutboundText( + widget.contact, + text, + ) + : null, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, - border: const OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + ), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerLow, contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + horizontal: 20, + vertical: 14, ), ), - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(connector), ); }, ), @@ -672,7 +733,11 @@ class _ChatScreenState extends State { } } final maxBytes = maxContactMessageBytes(); - if (utf8.encode(outgoingText).length > maxBytes) { + final outboundText = connector.prepareContactOutboundText( + _resolveContact(connector), + outgoingText, + ); + if (utf8.encode(outboundText).length > maxBytes) { showDismissibleSnackBar( context, content: Text(context.l10n.chat_messageTooLong(maxBytes)), @@ -1153,8 +1218,14 @@ class _ChatScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow(context.l10n.chat_type, contact.typeLabel), - _buildInfoRow(context.l10n.chat_path, contact.pathLabel), + _buildInfoRow( + context.l10n.chat_type, + contact.typeLabel(context.l10n), + ), + _buildInfoRow( + context.l10n.chat_path, + contact.pathLabel(context.l10n), + ), _buildInfoRow( context.l10n.contact_lastSeen, _formatContactLastMessage(contact.lastMessageAt), @@ -1305,11 +1376,15 @@ class _ChatScreenState extends State { } void _openChat(BuildContext context, Contact contact) { - // Check if this is a repeater - context.read().markContactRead(contact.publicKeyHex); + final connector = context.read(); + final unread = connector.getUnreadCountForContactKey(contact.publicKeyHex); + connector.markContactRead(contact.publicKeyHex); Navigator.push( context, - MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)), + MaterialPageRoute( + builder: (context) => + ChatScreen(contact: contact, initialUnreadCount: unread), + ), ); } @@ -1446,6 +1521,15 @@ class _ChatScreenState extends State { _copyMessageText(message.text); }, ), + if (!message.isOutgoing) + ListTile( + leading: const Icon(Icons.mark_chat_unread_outlined), + title: Text(context.l10n.chat_markAsUnread), + onTap: () { + Navigator.pop(sheetContext); + _markAsUnread(message); + }, + ), ListTile( leading: const Icon(Icons.delete_outline), title: Text(context.l10n.common_delete), @@ -1553,10 +1637,12 @@ class _MessageBubble extends StatelessWidget { final VoidCallback? onLongPress; final void Function(Message message, String emoji)? onRetryReaction; final double textScale; + final String sourceId; const _MessageBubble({ required this.message, required this.senderName, + required this.sourceId, required this.isRoomServer, required this.textScale, this.onTap, @@ -1571,7 +1657,7 @@ class _MessageBubble extends StatelessWidget { final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; final gifId = GifHelper.parseGif(message.text); - final poi = _parsePoiMessage(message.text); + final poi = parseMarkerText(message.text); final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed ? colorScheme.errorContainer @@ -1663,6 +1749,7 @@ class _MessageBubble extends StatelessWidget { textColor, metaColor, textScale, + senderName, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -1844,25 +1931,13 @@ class _MessageBubble extends StatelessWidget { ); } - _PoiInfo? _parsePoiMessage(String text) { - final trimmed = text.trim(); - final match = RegExp( - r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$', - ).firstMatch(trimmed); - if (match == null) return null; - final lat = double.tryParse(match.group(1) ?? ''); - final lon = double.tryParse(match.group(2) ?? ''); - if (lat == null || lon == null) return null; - final label = match.group(3) ?? ''; - return _PoiInfo(lat: lat, lon: lon, label: label); - } - Widget _buildPoiMessage( BuildContext context, - _PoiInfo poi, + MarkerPayload poi, Color textColor, Color metaColor, - double textScale, { + double textScale, + String senderName, { Widget? trailing, }) { return Row( @@ -1872,13 +1947,23 @@ class _MessageBubble extends StatelessWidget { icon: Icon(Icons.location_on_outlined, color: textColor), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - onPressed: () { + onPressed: () async { + final selfName = context.read().selfName ?? 'Me'; + final fromName = message.isOutgoing ? selfName : senderName; + final key = buildSharedMarkerKey( + sourceId: sourceId, + label: poi.label, + fromName: fromName, + flags: poi.flags, + isChannel: false, + ); Navigator.push( context, MaterialPageRoute( builder: (context) => MapScreen( - highlightPosition: LatLng(poi.lat, poi.lon), + highlightPosition: poi.position, highlightLabel: poi.label, + highlightMarkerKey: key, ), ), ); @@ -2059,11 +2144,3 @@ class _MessageBubble extends StatelessWidget { return '$hour:$minute'; } } - -class _PoiInfo { - final double lat; - final double lon; - final String label; - - const _PoiInfo({required this.lat, required this.lon, required this.label}); -} diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 54d32990..a4cc35ca 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,6 +13,7 @@ import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../models/contact_group.dart'; import '../services/ui_view_state_service.dart'; import '../utils/contact_search.dart'; @@ -930,10 +931,17 @@ class _ContactsScreenState extends State } else if (contact.type == advTypeRoom) { _showRoomLogin(context, contact, RoomLoginDestination.chat); } else { - context.read().markContactRead(contact.publicKeyHex); + final connector = context.read(); + final unread = connector.getUnreadCountForContactKey( + contact.publicKeyHex, + ); + connector.markContactRead(contact.publicKeyHex); Navigator.push( context, - MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)), + MaterialPageRoute( + builder: (context) => + ChatScreen(contact: contact, initialUnreadCount: unread), + ), ); } } @@ -988,7 +996,11 @@ class _ContactsScreenState extends State builder: (context) => RoomLoginDialog( room: room, onLogin: (password, isAdmin) { - context.read().markContactRead(room.publicKeyHex); + final connector = context.read(); + final unread = connector.getUnreadCountForContactKey( + room.publicKeyHex, + ); + connector.markContactRead(room.publicKeyHex); Navigator.push( context, MaterialPageRoute( @@ -999,7 +1011,7 @@ class _ContactsScreenState extends State password: password, isAdmin: isAdmin, ) - : ChatScreen(contact: room), + : ChatScreen(contact: room, initialUnreadCount: unread), ), ); }, @@ -1122,7 +1134,9 @@ class _ContactsScreenState extends State return CheckboxListTile( value: isSelected, title: Text(contact.name), - subtitle: Text(contact.typeLabel), + subtitle: Text( + contact.typeLabel(context.l10n), + ), onChanged: (value) { setDialogState(() { if (value == true) { @@ -1464,7 +1478,7 @@ class _ContactTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - contact.pathLabel, + contact.pathLabel(context.l10n), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index ec8a391f..3ba79d59 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -56,12 +57,16 @@ class _LineOfSightMapScreenState extends State { static const double _maxAntennaFeet = 400.0; static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet; static const double _labelZoomThreshold = 8.5; + static const double _mapMinZoom = 2.0; + static const double _mapMaxZoom = 18.0; final LineOfSightService _lineOfSightService = LineOfSightService(); + final MapController _mapController = MapController(); bool _loading = false; String? _error; LineOfSightPathResult? _result; + LineOfSightObstruction? _selectedObstruction; LineOfSightEndpoint? _start; LineOfSightEndpoint? _end; final List _customEndpoints = []; @@ -98,10 +103,85 @@ class _LineOfSightMapScreenState extends State { @override void dispose() { + _mapController.dispose(); _lineOfSightService.dispose(); super.dispose(); } + bool _isDesktopPlatform(TargetPlatform platform) { + return platform == TargetPlatform.linux || + platform == TargetPlatform.windows || + platform == TargetPlatform.macOS; + } + + void _zoomMapBy(double delta) { + final camera = _mapController.camera; + final nextZoom = (camera.zoom + delta) + .clamp(_mapMinZoom, _mapMaxZoom) + .toDouble(); + _mapController.move(camera.center, nextZoom); + } + + void _resetMapView({ + required LatLng initialCenter, + required double initialZoom, + required LatLngBounds? bounds, + }) { + if (bounds != null) { + _mapController.fitCamera( + CameraFit.bounds( + bounds: bounds, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + ); + return; + } + _mapController.move(initialCenter, initialZoom); + } + + Widget _buildDesktopMapControls({ + required LatLng initialCenter, + required double initialZoom, + required LatLngBounds? bounds, + }) { + final screenHeight = MediaQuery.of(context).size.height; + final topOffset = _showHud + ? math.min(screenHeight * 0.52 + 24, screenHeight - 220) + : 12.0; + return Positioned( + top: topOffset, + left: 12, + child: Card( + elevation: 4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: 'Zoom in', + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: 'Zoom out', + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: 'Center map', + onPressed: () => _resetMapView( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + ), + ), + ], + ), + ), + ); + } + Future _runLos() async { final start = _start; final end = _end; @@ -111,6 +191,7 @@ class _LineOfSightMapScreenState extends State { if (start == null || end == null) { setState(() { _result = null; + _selectedObstruction = null; _error = _errorSelectStartEnd; }); return; @@ -142,6 +223,7 @@ class _LineOfSightMapScreenState extends State { } setState(() { _result = result; + _selectedObstruction = _defaultObstructionFor(result); }); } catch (e) { if (!mounted) return; @@ -156,6 +238,7 @@ class _LineOfSightMapScreenState extends State { } setState(() { _result = null; + _selectedObstruction = null; _error = context.l10n.losRunFailed(e.toString()); }); } finally { @@ -184,6 +267,7 @@ class _LineOfSightMapScreenState extends State { void _selectFromMap(LineOfSightEndpoint endpoint) { setState(() { _result = null; + _selectedObstruction = null; _error = null; if (_start == null || (_start != null && _end != null)) { _start = endpoint; @@ -241,6 +325,7 @@ class _LineOfSightMapScreenState extends State { _start = null; _end = null; _result = null; + _selectedObstruction = null; _error = _errorSelectStartEnd; }); } @@ -251,6 +336,7 @@ class _LineOfSightMapScreenState extends State { if (identical(_start, endpoint)) _start = null; if (identical(_end, endpoint)) _end = null; _result = null; + _selectedObstruction = null; }); } @@ -318,6 +404,7 @@ class _LineOfSightMapScreenState extends State { ? LatLngBounds.fromPoints(mapPoints) : null; final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0; + final isDesktop = _isDesktopPlatform(defaultTargetPlatform); if (!_didReceivePositionUpdate) { _showMarkerLabels = initialZoom >= _labelZoomThreshold; } @@ -343,6 +430,7 @@ class _LineOfSightMapScreenState extends State { body: Stack( children: [ FlutterMap( + mapController: _mapController, options: MapOptions( initialCenter: initialCenter, initialZoom: initialZoom, @@ -355,7 +443,19 @@ class _LineOfSightMapScreenState extends State { ), interactionOptions: InteractionOptions( flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), ), + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, onLongPress: (_, point) => _addCustomPoint(point), onPositionChanged: (camera, hasGesture) { final shouldShow = camera.zoom >= _labelZoomThreshold; @@ -377,9 +477,17 @@ class _LineOfSightMapScreenState extends State { ), if (_result != null && _result!.segments.isNotEmpty) PolylineLayer(polylines: _buildSegmentPolylines(_result!)), - MarkerLayer(markers: _buildMarkers(endpoints)), + MarkerLayer( + markers: _buildMarkers(endpoints, _primaryObstructions()), + ), ], ), + if (isDesktop) + _buildDesktopMapControls( + initialCenter: initialCenter, + initialZoom: initialZoom, + bounds: bounds, + ), if (_showHud) Positioned( left: 12, @@ -445,6 +553,8 @@ class _LineOfSightMapScreenState extends State { ); final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz; final kFactorUsed = segment?.usedKFactor; + final obstructions = + segment?.obstructions ?? const []; final endpoints = _visibleEndpoints(); final distanceUnit = isImperial ? 'mi' : 'km'; final heightUnit = isImperial ? 'ft' : 'm'; @@ -463,31 +573,7 @@ class _LineOfSightMapScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (segment != null) - SizedBox( - height: 160, - width: double.infinity, - child: CustomPaint( - painter: _LosProfilePainter( - samples: segment.samples, - distanceUnit: distanceUnit, - heightUnit: heightUnit, - badgeTextStyle: - Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.white70, - fontSize: 10, - fontWeight: FontWeight.w600, - ) ?? - const TextStyle( - color: Colors.white70, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - terrainLabel: context.l10n.losLegendTerrain, - losBeamLabel: context.l10n.losLegendLosBeam, - radioHorizonLabel: context.l10n.losLegendRadioHorizon, - ), - ), - ) + _buildProfileView(segment, distanceUnit, heightUnit, isImperial) else SizedBox( height: 44, @@ -519,6 +605,96 @@ class _LineOfSightMapScreenState extends State { fontWeight: FontWeight.w600, ), ), + if (obstructions.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + context.l10n.losBlockedSpotsTitle, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.losBlockedSpotsHint, + style: TextStyle(fontSize: 11, color: Colors.grey[700]), + ), + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + for (final obstruction in obstructions) + ChoiceChip( + label: Text( + _obstructionChipLabel(obstruction, isImperial), + style: const TextStyle(fontSize: 11), + ), + selected: + _selectedObstruction?.sampleIndex == + obstruction.sampleIndex, + onSelected: (_) => _selectObstruction(obstruction), + ), + ], + ), + if (_selectedObstruction != null) ...[ + const SizedBox(height: 8), + DecoratedBox( + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.deepOrangeAccent.withValues(alpha: 0.45), + ), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.losSelectedObstructionTitle, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.losSelectedObstructionDetails( + _formatHeightValue( + _selectedObstruction!.obstructionMeters, + isImperial, + ), + heightUnit, + _formatDistanceValue( + _selectedObstruction!.distanceMeters, + isImperial, + ), + distanceUnit, + _formatDistanceValue( + segment!.totalDistanceMeters - + _selectedObstruction!.distanceMeters, + isImperial, + ), + ), + style: const TextStyle(fontSize: 11), + ), + const SizedBox(height: 4), + Text( + '${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, ' + '${_selectedObstruction!.point.longitude.toStringAsFixed(5)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + ), + ), + ], + ), + ), + ), + ], + ], const SizedBox(height: 4), if (displayFrequencyMHz != null) Padding( @@ -605,6 +781,7 @@ class _LineOfSightMapScreenState extends State { _showDisplayNodes = value; _sanitizeSelection(); _result = null; + _selectedObstruction = null; }); }, ), @@ -655,6 +832,7 @@ class _LineOfSightMapScreenState extends State { setState(() { _start = value; _result = null; + _selectedObstruction = null; }); if (_start != null && _end != null) { _runLos(); @@ -670,6 +848,7 @@ class _LineOfSightMapScreenState extends State { setState(() { _end = value; _result = null; + _selectedObstruction = null; }); if (_start != null && _end != null) { _runLos(); @@ -769,6 +948,179 @@ class _LineOfSightMapScreenState extends State { return _result!.segments.first.result; } + List _primaryObstructions() { + return _primarySegmentResult()?.obstructions ?? const []; + } + + LineOfSightObstruction? _defaultObstructionFor( + LineOfSightPathResult? result, + ) { + if (result == null || result.segments.isEmpty) return null; + final obstructions = result.segments.first.result.obstructions; + if (obstructions.isEmpty) return null; + return obstructions.reduce( + (current, next) => + next.obstructionMeters > current.obstructionMeters ? next : current, + ); + } + + void _selectObstruction(LineOfSightObstruction obstruction) { + setState(() { + _selectedObstruction = obstruction; + }); + } + + String _formatDistanceValue(double meters, bool isImperial) { + final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0; + return value.toStringAsFixed(2); + } + + String _formatHeightValue(double meters, bool isImperial) { + final value = isImperial ? meters * _metersToFeet : meters; + return value.toStringAsFixed(1); + } + + String _obstructionChipLabel( + LineOfSightObstruction obstruction, + bool isImperial, + ) { + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + return context.l10n.losBlockedSpotChip( + _formatDistanceValue(obstruction.distanceMeters, isImperial), + distanceUnit, + _formatHeightValue(obstruction.obstructionMeters, isImperial), + heightUnit, + ); + } + + Widget _buildProfileView( + LineOfSightResult segment, + String distanceUnit, + String heightUnit, + bool isImperial, + ) { + if (segment.samples.length < 2) { + return SizedBox( + height: 160, + width: double.infinity, + child: CustomPaint( + painter: _LosProfilePainter( + samples: segment.samples, + distanceUnit: distanceUnit, + heightUnit: heightUnit, + badgeTextStyle: + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + selectedSampleIndex: _selectedObstruction?.sampleIndex, + ), + ), + ); + } + return SizedBox( + height: 160, + width: double.infinity, + child: LayoutBuilder( + builder: (context, constraints) { + final size = Size(constraints.maxWidth, 160); + final geometry = _LosProfileGeometry( + samples: segment.samples, + size: size, + ); + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: CustomPaint( + painter: _LosProfilePainter( + samples: segment.samples, + distanceUnit: distanceUnit, + heightUnit: heightUnit, + badgeTextStyle: + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + selectedSampleIndex: _selectedObstruction?.sampleIndex, + ), + ), + ), + for (final obstruction in segment.obstructions) + Builder( + builder: (context) { + final sample = segment.samples[obstruction.sampleIndex]; + final position = geometry.mapPoint( + sample.distanceMeters, + sample.terrainMeters, + ); + final isSelected = + _selectedObstruction?.sampleIndex == + obstruction.sampleIndex; + final markerSize = isSelected ? 18.0 : 14.0; + final left = (position.dx - markerSize / 2) + .clamp(0.0, math.max(0.0, size.width - markerSize)) + .toDouble(); + final top = (position.dy - markerSize / 2) + .clamp(0.0, math.max(0.0, size.height - markerSize)) + .toDouble(); + return Positioned( + left: left, + top: top, + child: Tooltip( + message: _obstructionChipLabel(obstruction, isImperial), + child: GestureDetector( + onTap: () => _selectObstruction(obstruction), + child: Container( + width: markerSize, + height: markerSize, + decoration: BoxDecoration( + color: isSelected + ? Colors.amberAccent + : Colors.deepOrangeAccent, + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Colors.white + : Colors.black87, + width: isSelected ? 2 : 1.5, + ), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 4), + ], + ), + ), + ), + ), + ); + }, + ), + ], + ); + }, + ), + ); + } + String _profileStats(LineOfSightResult result, bool isImperial) { final distance = isImperial ? (result.totalDistanceMeters / 1000.0) * _kmToMiles @@ -820,8 +1172,51 @@ class _LineOfSightMapScreenState extends State { return polylines; } - List _buildMarkers(List endpoints) { + List _buildMarkers( + List endpoints, + List obstructions, + ) { return [ + for (final obstruction in obstructions) + Marker( + point: obstruction.point, + width: 52, + height: 52, + child: GestureDetector( + onTap: () => _selectObstruction(obstruction), + child: Center( + child: Container( + width: + _selectedObstruction?.sampleIndex == obstruction.sampleIndex + ? 36 + : 24, + height: + _selectedObstruction?.sampleIndex == obstruction.sampleIndex + ? 36 + : 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + border: Border.all( + color: + _selectedObstruction?.sampleIndex == + obstruction.sampleIndex + ? Colors.amberAccent + : Colors.deepOrangeAccent, + width: + _selectedObstruction?.sampleIndex == + obstruction.sampleIndex + ? 4 + : 3, + ), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 6), + ], + ), + ), + ), + ), + ), for (final endpoint in endpoints) Marker( point: endpoint.point, @@ -1010,6 +1405,51 @@ class _LineOfSightMapScreenState extends State { } } +class _LosProfileGeometry { + static const horizontalPadding = 12.0; + static const verticalPadding = 12.0; + + final List samples; + final Size size; + late final double minY = samples + .map( + (s) => math.min( + math.min(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) + .reduce(math.min); + late final double maxY = samples + .map( + (s) => math.max( + math.max(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) + .reduce(math.max); + late final double ySpan = math.max(1.0, maxY - minY); + late final double maxDist = math.max(1.0, samples.last.distanceMeters); + late final double chartWidth = math.max( + 1.0, + size.width - horizontalPadding * 2, + ); + late final double chartHeight = math.max( + 1.0, + size.height - verticalPadding * 2, + ); + + _LosProfileGeometry({required this.samples, required this.size}); + + Offset mapPoint(double distanceMeters, double elevationMeters) { + final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth; + final py = + size.height - + verticalPadding - + ((elevationMeters - minY) / ySpan) * chartHeight; + return Offset(px, py); + } +} + class _LosProfilePainter extends CustomPainter { final List samples; final String distanceUnit; @@ -1018,6 +1458,7 @@ class _LosProfilePainter extends CustomPainter { final String terrainLabel; final String losBeamLabel; final String radioHorizonLabel; + final int? selectedSampleIndex; const _LosProfilePainter({ required this.samples, @@ -1027,6 +1468,7 @@ class _LosProfilePainter extends CustomPainter { required this.terrainLabel, required this.losBeamLabel, required this.radioHorizonLabel, + this.selectedSampleIndex, }); @override @@ -1212,6 +1654,32 @@ class _LosProfilePainter extends CustomPainter { ..color = horizonFillColor ..style = PaintingStyle.fill, ); + + if (selectedSampleIndex != null && + selectedSampleIndex! >= 0 && + selectedSampleIndex! < samples.length) { + final selectedSample = samples[selectedSampleIndex!]; + final selectedPoint = mapPoint( + selectedSample.distanceMeters, + selectedSample.terrainMeters, + ); + canvas.drawLine( + Offset(selectedPoint.dx, verticalPadding), + Offset(selectedPoint.dx, size.height - verticalPadding), + Paint() + ..color = Colors.amberAccent.withValues(alpha: 0.7) + ..strokeWidth = 1.5, + ); + canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent); + canvas.drawCircle( + selectedPoint, + 8.5, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + } } @override @@ -1222,7 +1690,8 @@ class _LosProfilePainter extends CustomPainter { oldDelegate.badgeTextStyle != badgeTextStyle || oldDelegate.terrainLabel != terrainLabel || oldDelegate.losBeamLabel != losBeamLabel || - oldDelegate.radioHorizonLabel != radioHorizonLabel; + oldDelegate.radioHorizonLabel != radioHorizonLabel || + oldDelegate.selectedSampleIndex != selectedSampleIndex; } void _drawUnitBadge(Canvas canvas, Size size) { diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 1eb59a86..4057e0ec 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -18,6 +19,9 @@ class MapCacheScreen extends StatefulWidget { } class _MapCacheScreenState extends State { + static const double _mapMinZoom = 2.0; + static const double _mapMaxZoom = 18.0; + final MapController _mapController = MapController(); LatLngBounds? _selectedBounds; @@ -43,6 +47,61 @@ class _MapCacheScreenState extends State { super.dispose(); } + bool _isDesktopPlatform(TargetPlatform platform) { + return platform == TargetPlatform.linux || + platform == TargetPlatform.windows || + platform == TargetPlatform.macOS; + } + + void _zoomMapBy(double delta) { + final camera = _mapController.camera; + final nextZoom = (camera.zoom + delta) + .clamp(_mapMinZoom, _mapMaxZoom) + .toDouble(); + _mapController.move(camera.center, nextZoom); + } + + void _resetMapView() { + final bounds = _selectedBounds; + if (bounds != null) { + _mapController.fitCamera( + CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)), + ); + return; + } + _mapController.move(const LatLng(0, 0), 2.0); + } + + Widget _buildDesktopMapControls() { + return Positioned( + top: 12, + left: 12, + child: Card( + elevation: 4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: 'Zoom in', + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: 'Zoom out', + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: 'Center map', + onPressed: _resetMapView, + ), + ], + ), + ), + ); + } + void _loadSettings() { final settings = context.read().settings; final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds); @@ -222,6 +281,7 @@ class _MapCacheScreenState extends State { final tileCache = context.read(); final selectedBounds = _selectedBounds; final l10n = context.l10n; + final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final progressValue = _estimatedTiles == 0 ? 0.0 : (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble(); @@ -238,11 +298,24 @@ class _MapCacheScreenState extends State { children: [ FlutterMap( mapController: _mapController, - options: const MapOptions( - initialCenter: LatLng(0, 0), + options: MapOptions( + initialCenter: const LatLng(0, 0), initialZoom: 2.0, - minZoom: 2.0, - maxZoom: 18.0, + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), + ), ), children: [ TileLayer( @@ -265,6 +338,7 @@ class _MapCacheScreenState extends State { ), ], ), + if (isDesktop) _buildDesktopMapControls(), Positioned( top: 12, right: 12, diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 6a8acda7..9108e2b4 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,6 +1,5 @@ import 'dart:collection'; import 'dart:math'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -16,6 +15,7 @@ import '../connector/meshcore_protocol.dart'; import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; +import '../l10n/contact_localization.dart'; import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; @@ -37,6 +37,7 @@ import 'line_of_sight_map_screen.dart'; class MapScreen extends StatefulWidget { final LatLng? highlightPosition; final String? highlightLabel; + final String? highlightMarkerKey; final double highlightZoom; final bool hideBackButton; @@ -44,6 +45,7 @@ class MapScreen extends StatefulWidget { super.key, this.highlightPosition, this.highlightLabel, + this.highlightMarkerKey, this.highlightZoom = 15.0, this.hideBackButton = false, }); @@ -55,6 +57,8 @@ class MapScreen extends StatefulWidget { class _MapScreenState extends State { // Zoom level at which node labels start to appear static const double _labelZoomThreshold = 14.0; + static const double _mapMinZoom = 2.0; + static const double _mapMaxZoom = 18.0; final MapController _mapController = MapController(); final MapMarkerService _markerService = MapMarkerService(); @@ -94,6 +98,19 @@ class _MapScreenState extends State { _removedMarkerIds = ids; _removedMarkersLoaded = true; }); + // If this screen was opened to highlight a marker, and that marker + // was previously removed, re-enable it now that we've loaded the saved + // removed IDs. + if (widget.highlightMarkerKey != null && + _removedMarkerIds.contains(widget.highlightMarkerKey)) { + final updated = Set.from(_removedMarkerIds); + updated.remove(widget.highlightMarkerKey); + if (!mounted) return; + setState(() { + _removedMarkerIds = updated; + }); + await _markerService.saveRemovedIds(updated); + } } bool _checkLocationPlausibility(double lat, double lon) { @@ -134,11 +151,62 @@ class _MapScreenState extends State { return zoom.clamp(4.0, 15.0); } + bool _isDesktopPlatform(TargetPlatform platform) { + return platform == TargetPlatform.linux || + platform == TargetPlatform.windows || + platform == TargetPlatform.macOS; + } + + void _zoomMapBy(double delta) { + final camera = _mapController.camera; + final nextZoom = (camera.zoom + delta) + .clamp(_mapMinZoom, _mapMaxZoom) + .toDouble(); + _mapController.move(camera.center, nextZoom); + } + + Widget _buildDesktopMapControls( + BuildContext context, { + required LatLng center, + required double zoom, + required bool hasPathSelector, + }) { + return Positioned( + left: 16, + top: hasPathSelector ? null : 16, + bottom: hasPathSelector ? 16 : null, + child: Card( + elevation: 4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: 'Zoom in', + onPressed: () => _zoomMapBy(1), + ), + IconButton( + icon: const Icon(Icons.remove), + tooltip: 'Zoom out', + onPressed: () => _zoomMapBy(-1), + ), + IconButton( + icon: const Icon(Icons.my_location), + tooltip: 'Center map', + onPressed: () => _mapController.move(center, zoom), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Consumer3( builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); + final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final settings = settingsService.settings; final allContacts = connector.allContacts; @@ -229,6 +297,24 @@ class _MapScreenState extends State { : [], ); + // Collect polylines for shared markers' history with dashed lines + final List sharedMarkerPolylines = []; + for (final marker in sharedMarkers) { + if (marker.history.isNotEmpty) { + final points = List.from(marker.history); + points.add(marker.position); + sharedMarkerPolylines.add( + Polyline( + points: points, + color: marker.isChannel + ? (marker.isPublicChannel ? Colors.orange : Colors.purple) + : Colors.blue, + strokeWidth: 3, + ), + ); + } + } + // Calculate center and zoom of all nodes, or default to (0, 0) LatLng center = const LatLng(0, 0); double initialZoom = 10.0; @@ -417,10 +503,20 @@ class _MapScreenState extends State { options: MapOptions( initialCenter: center, initialZoom: initialZoom, - minZoom: 2.0, - maxZoom: 18.0, + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, interactionOptions: InteractionOptions( flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), ), onTap: (_, latLng) { if (_isSelectingPoi) { @@ -475,6 +571,8 @@ class _MapScreenState extends State { ), if (_polylines.isNotEmpty && _isBuildingPathTrace) PolylineLayer(polylines: _polylines), + if (sharedMarkerPolylines.isNotEmpty) + PolylineLayer(polylines: sharedMarkerPolylines), MarkerLayer( markers: [ if (highlightPosition != null) @@ -562,6 +660,13 @@ class _MapScreenState extends State { sharedMarkers.length, guessedLocations.length, ), + if (isDesktop) + _buildDesktopMapControls( + context, + center: center, + zoom: initialZoom, + hasPathSelector: _isBuildingPathTrace, + ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], ), @@ -1239,28 +1344,39 @@ class _MapScreenState extends State { } List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) { - final markers = <_SharedMarker>[]; + // Build a _SharedMarker per message (history empty), grouped by dedupe key. + // Afterwards pick the latest per key and fill its history from older ones. + final updatesByKey = >{}; final selfName = connector.selfName ?? 'Me'; + void addUpdate(_SharedMarker update) { + (updatesByKey[update.id] ??= <_SharedMarker>[]).add(update); + } + for (final contact in connector.contacts) { final messages = connector.getMessages(contact); for (final message in messages) { - final payload = _parseMarkerText(message.text); + final payload = parseMarkerText(message.text); if (payload == null) continue; final fromName = message.isOutgoing ? selfName : contact.name; - final id = _buildMarkerId( + final key = buildSharedMarkerKey( sourceId: contact.publicKeyHex, - timestamp: message.timestamp, - text: message.text, + label: payload.label, + fromName: fromName, + flags: payload.flags, + isChannel: false, ); - markers.add( + addUpdate( _SharedMarker( - id: id, + id: key, position: payload.position, - label: payload.label, + label: payload.label.isEmpty + ? context.l10n.map_sharedPin + : payload.label, flags: payload.flags, fromName: fromName, sourceLabel: contact.name, + timestamp: message.timestamp, isChannel: false, isPublicChannel: false, ), @@ -1272,23 +1388,28 @@ class _MapScreenState extends State { final isPublic = _isPublicChannel(channel); final messages = connector.getChannelMessages(channel); for (final message in messages) { - final payload = _parseMarkerText(message.text); + final payload = parseMarkerText(message.text); if (payload == null) continue; - final id = _buildMarkerId( + final key = buildSharedMarkerKey( sourceId: 'channel:${channel.index}', - timestamp: message.timestamp, - text: message.text, + label: payload.label, + fromName: message.senderName, + flags: payload.flags, + isChannel: true, ); - markers.add( + addUpdate( _SharedMarker( - id: id, + id: key, position: payload.position, - label: payload.label, + label: payload.label.isEmpty + ? context.l10n.map_sharedPin + : payload.label, flags: payload.flags, fromName: message.senderName, sourceLabel: channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name, + timestamp: message.timestamp, isChannel: true, isPublicChannel: isPublic, ), @@ -1296,38 +1417,27 @@ class _MapScreenState extends State { } } + final markers = <_SharedMarker>[]; + updatesByKey.forEach((_, updates) { + updates.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + final latest = updates.last; + // History: older positions, drop consecutive duplicates at same position. + final history = []; + for (var i = 0; i < updates.length - 1; i++) { + final p = updates[i].position; + if (history.isEmpty || + history.last.latitude != p.latitude || + history.last.longitude != p.longitude) { + history.add(p); + } + } + markers.add(latest.copyWithHistory(history)); + }); + + markers.sort((a, b) => b.timestamp.compareTo(a.timestamp)); return markers; } - _MarkerPayload? _parseMarkerText(String text) { - final trimmed = text.trim(); - if (!trimmed.startsWith('m:')) return null; - - final parts = trimmed.substring(2).split('|'); - if (parts.isEmpty) return null; - final coords = parts[0].split(','); - if (coords.length != 2) return null; - final lat = double.tryParse(coords[0].trim()); - final lon = double.tryParse(coords[1].trim()); - if (lat == null || lon == null) return null; - - final label = parts.length > 1 ? parts[1].trim() : ''; - final flags = parts.length > 2 ? parts[2].trim() : ''; - return _MarkerPayload( - position: LatLng(lat, lon), - label: label.isEmpty ? context.l10n.map_sharedPin : label, - flags: flags, - ); - } - - String _buildMarkerId({ - required String sourceId, - required DateTime timestamp, - required String text, - }) { - return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text'; - } - Marker _buildSharedMarker(_SharedMarker marker) { final markerColor = marker.isChannel ? (marker.isPublicChannel ? Colors.orange : Colors.purple) @@ -1337,7 +1447,15 @@ class _MapScreenState extends State { width: 60, height: 60, child: GestureDetector( - onTap: () => _showMarkerInfo(marker), + onTap: () async { + if (_removedMarkerIds.contains(marker.id)) { + setState(() { + _removedMarkerIds.remove(marker.id); + }); + await _markerService.saveRemovedIds(_removedMarkerIds); + } + _showMarkerInfo(marker); + }, child: Column( children: [ Container( @@ -1391,11 +1509,17 @@ class _MapScreenState extends State { room: room, // onLogin(password, isAdmin) isAdmin not used for room caht screen onLogin: (password, _) { - // Navigate to chat screen after successful login - context.read().markContactRead(room.publicKeyHex); + final connector = context.read(); + final unread = connector.getUnreadCountForContactKey( + room.publicKeyHex, + ); + connector.markContactRead(room.publicKeyHex); Navigator.push( context, - MaterialPageRoute(builder: (context) => ChatScreen(contact: room)), + MaterialPageRoute( + builder: (context) => + ChatScreen(contact: room, initialUnreadCount: unread), + ), ); }, ), @@ -1425,23 +1549,29 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildInfoRow('Type', contact.typeLabel), - _buildInfoRow('Path', contact.pathLabel), + _buildInfoRow( + context.l10n.map_type, + contact.typeLabel(context.l10n), + ), + _buildInfoRow( + context.l10n.map_path, + contact.pathLabel(context.l10n), + ), if (contact.hasLocation) _buildInfoRow( - 'Location', + context.l10n.map_location, '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', ) else if (guessedPosition != null) _buildInfoRow( - 'Est. Location', + context.l10n.map_estLocation, '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', ), _buildInfoRow( context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen), ), - _buildInfoRow('Public Key', contact.publicKeyHex), + _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), ], ), actions: [ @@ -1456,11 +1586,17 @@ class _MapScreenState extends State { if (!contact.isActive) { connector.importDiscoveredContact(contact); } + final unread = connector.getUnreadCountForContactKey( + contact.publicKeyHex, + ); Navigator.pop(dialogContext); Navigator.push( context, MaterialPageRoute( - builder: (context) => ChatScreen(contact: contact), + builder: (context) => ChatScreen( + contact: contact, + initialUnreadCount: unread, + ), ), ); }, @@ -1542,7 +1678,9 @@ class _MapScreenState extends State { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: Text(marker.label), + title: Text( + marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label, + ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -1550,7 +1688,11 @@ class _MapScreenState extends State { _buildInfoRow(context.l10n.map_from, marker.fromName), _buildInfoRow(context.l10n.map_source, marker.sourceLabel), _buildInfoRow( - 'Location', + context.l10n.map_sharedAt, + _formatLastSeen(marker.timestamp), + ), + _buildInfoRow( + context.l10n.map_location, '${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}', ), if (marker.flags.isNotEmpty) @@ -1715,6 +1857,10 @@ class _MapScreenState extends State { String defaultLabel, ) async { final controller = TextEditingController(text: defaultLabel); + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); return showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -2019,7 +2165,7 @@ class _MapScreenState extends State { enabled: settings.mapKeyPrefixEnabled, decoration: InputDecoration( labelText: context.l10n.map_publicKeyPrefix, - hintText: 'e.g. ab12', + hintText: context.l10n.map_publicKeyPrefixHint, border: const OutlineInputBorder(), isDense: true, ), @@ -2310,18 +2456,50 @@ class _GuessedLocation { }); } -class _MarkerPayload { +class MarkerPayload { final LatLng position; final String label; final String flags; - _MarkerPayload({ + MarkerPayload({ required this.position, required this.label, required this.flags, }); } +/// Parse a shared marker text message of the form +/// `m:,|