chore/offband-rebrand
HDDen 3 months ago
commit ca6058eccd

1
.gitignore vendored

@ -87,6 +87,7 @@ keystore.properties
# IDE # IDE
.vscode/launch.json .vscode/launch.json
.vscode/settings.json .vscode/settings.json
.contextstream/
# Cloudflare Wrangler # Cloudflare Wrangler
.wrangler .wrangler

@ -677,6 +677,27 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
void setContactUnreadCount(String contactKeyHex, int count) {
_contactUnreadCount[contactKeyHex] = count;
_unreadStore.saveContactUnreadCount(
Map<String, int>.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) { void markChannelRead(int channelIndex) {
final channel = _findChannelByIndex(channelIndex); final channel = _findChannelByIndex(channelIndex);
if (channel != null && channel.unreadCount > 0) { if (channel != null && channel.unreadCount > 0) {
@ -4043,7 +4064,7 @@ class MeshCoreConnector extends ChangeNotifier {
); );
} else { } else {
appLogger.info( 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', tag: 'Connector',
); );
return; return;
@ -4065,7 +4086,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
_notificationService.showAdvertNotification( _notificationService.showAdvertNotification(
contactName: contact.name, contactName: contact.name,
contactType: contact.typeLabel, contactType: contact.typeLabelRaw,
contactId: contact.publicKeyHex, contactId: contact.publicKeyHex,
); );
} }
@ -4140,7 +4161,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
_notificationService.showAdvertNotification( _notificationService.showAdvertNotification(
contactName: contact.name, contactName: contact.name,
contactType: contact.typeLabel, contactType: contact.typeLabelRaw,
contactId: contact.publicKeyHex, contactId: contact.publicKeyHex,
); );
} }
@ -6173,7 +6194,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
_notificationService.showAdvertNotification( _notificationService.showAdvertNotification(
contactName: contact.name, contactName: contact.name,
contactType: contact.typeLabel, contactType: contact.typeLabelRaw,
contactId: contact.publicKeyHex, contactId: contact.publicKeyHex,
); );
} }

@ -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() { void scrollToBottomIfAtBottom() {
// Only scroll if jump button is NOT showing (i.e., already at bottom) // Only scroll if jump button is NOT showing (i.e., already at bottom)
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) { if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Режим на поверителност е активиран", "settings_privacyModeEnabled": "Режим на поверителност е активиран",
"settings_privacyModeDisabled": "Режим на поверителност е деактивиран", "settings_privacyModeDisabled": "Режим на поверителност е деактивиран",
"settings_actions": "Действия", "settings_actions": "Действия",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Изпрати Реклама", "settings_sendAdvertisement": "Изпрати Реклама",
"settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир",
"settings_advertisementSent": "Реклама изпратена", "settings_advertisementSent": "Реклама изпратена",
@ -2082,5 +2084,86 @@
"room_guest": "Информация за сървъра на стаята", "room_guest": "Информация за сървъра на стаята",
"repeater_guest": "Информация за ретранслаторите", "repeater_guest": "Информация за ретранслаторите",
"repeater_guestTools": "Инструменти за гости", "repeater_guestTools": "Инструменти за гости",
"settings_multiAck": "Множество потвърждения" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
"settings_actions": "Aktionen", "settings_actions": "Aktionen",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Sende Ankündigung", "settings_sendAdvertisement": "Sende Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
"settings_advertisementSent": "Ankündigung gesendet", "settings_advertisementSent": "Ankündigung gesendet",
@ -2110,5 +2112,86 @@
"repeater_guestTools": "Gastwerkzeuge", "repeater_guestTools": "Gastwerkzeuge",
"chat_sendMessage": "Nachricht senden", "chat_sendMessage": "Nachricht senden",
"room_guest": "Informationen zum Room Server", "room_guest": "Informationen zum Room Server",
"settings_multiAck": "Mehrere Bestätigungen" "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})."
} }

@ -12,6 +12,7 @@
"common_delete": "Delete", "common_delete": "Delete",
"common_deleteAll": "Delete All", "common_deleteAll": "Delete All",
"common_close": "Close", "common_close": "Close",
"common_done": "Done",
"common_edit": "Edit", "common_edit": "Edit",
"common_add": "Add", "common_add": "Add",
"common_settings": "Settings", "common_settings": "Settings",
@ -181,6 +182,8 @@
"settings_multiAck": "Multi-ACKs", "settings_multiAck": "Multi-ACKs",
"settings_telemetryModeUpdated": "Telemetry mode updated", "settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions", "settings_actions": "Actions",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Send Advertisement", "settings_sendAdvertisement": "Send Advertisement",
"settings_sendAdvertisementSubtitle": "Broadcast presence now", "settings_sendAdvertisementSubtitle": "Broadcast presence now",
"settings_advertisementSent": "Advertisement sent", "settings_advertisementSent": "Advertisement sent",
@ -516,6 +519,14 @@
}, },
"channels_hashtagChannel": "Hashtag channel", "channels_hashtagChannel": "Hashtag channel",
"channels_public": "Public", "channels_public": "Public",
"channels_via": "via {path}",
"@channels_via": {
"placeholders": {
"path": {
"type": "String"
}
}
},
"channels_private": "Private", "channels_private": "Private",
"channels_publicChannel": "Public channel", "channels_publicChannel": "Public channel",
"channels_privateChannel": "Private channel", "channels_privateChannel": "Private channel",
@ -794,6 +805,7 @@
} }
}, },
"chat_successes": "successes", "chat_successes": "successes",
"chat_score": "Score",
"chat_removePath": "Remove path", "chat_removePath": "Remove path",
"chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.", "chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.",
"chat_pathActions": "Path Actions:", "chat_pathActions": "Path Actions:",
@ -845,6 +857,8 @@
} }
} }
}, },
"chat_markAsUnread": "Mark as Unread",
"chat_newMessages": "New messages",
"chat_openLink": "Open Link?", "chat_openLink": "Open Link?",
"chat_openLinkConfirmation": "Do you want to open this link in your browser?", "chat_openLinkConfirmation": "Do you want to open this link in your browser?",
"chat_open": "Open", "chat_open": "Open",
@ -890,6 +904,12 @@
"map_from": "From", "map_from": "From",
"map_source": "Source", "map_source": "Source",
"map_flags": "Flags", "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_shareMarkerHere": "Share marker here",
"map_setAsMyLocation": "Set as my location", "map_setAsMyLocation": "Set as my location",
"map_pinLabel": "Pin label", "map_pinLabel": "Pin label",
@ -924,6 +944,7 @@
"map_guessedLocation": "Guessed location", "map_guessedLocation": "Guessed location",
"map_lastSeenTime": "Last Seen Time", "map_lastSeenTime": "Last Seen Time",
"map_sharedPin": "Shared pin", "map_sharedPin": "Shared pin",
"map_sharedAt": "Shared",
"map_joinRoom": "Join Room", "map_joinRoom": "Join Room",
"map_manageRepeater": "Manage Repeater", "map_manageRepeater": "Manage Repeater",
"map_tapToAdd": "Tap on nodes to add them to the path.", "map_tapToAdd": "Tap on nodes to add them to the path.",
@ -1903,6 +1924,46 @@
"losLegendRadioHorizon": "Radio horizon", "losLegendRadioHorizon": "Radio horizon",
"losLegendLosBeam": "LOS beam", "losLegendLosBeam": "LOS beam",
"losLegendTerrain": "Terrain", "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", "losFrequencyLabel": "Frequency",
"losFrequencyInfoTooltip": "View calculation details", "losFrequencyInfoTooltip": "View calculation details",
"losFrequencyDialogTitle": "Radio horizon calculation", "losFrequencyDialogTitle": "Radio horizon calculation",
@ -2142,5 +2203,36 @@
} }
}, },
"translation_translationOptions": "Translation options", "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"
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Modo de privacidad activado", "settings_privacyModeEnabled": "Modo de privacidad activado",
"settings_privacyModeDisabled": "Modo de privacidad desactivado", "settings_privacyModeDisabled": "Modo de privacidad desactivado",
"settings_actions": "Acciones", "settings_actions": "Acciones",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Enviar Anuncio", "settings_sendAdvertisement": "Enviar Anuncio",
"settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora",
"settings_advertisementSent": "Anuncio enviado", "settings_advertisementSent": "Anuncio enviado",
@ -2110,5 +2112,86 @@
"chat_sendMessage": "Enviar mensaje", "chat_sendMessage": "Enviar mensaje",
"repeater_guestTools": "Herramientas para invitados", "repeater_guestTools": "Herramientas para invitados",
"room_guest": "Información del servidor", "room_guest": "Información del servidor",
"settings_multiAck": "Múltiples respuestas de confirmación" "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}"
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Mode de confidentialité activé", "settings_privacyModeEnabled": "Mode de confidentialité activé",
"settings_privacyModeDisabled": "Mode de confidentialité désactivé", "settings_privacyModeDisabled": "Mode de confidentialité désactivé",
"settings_actions": "Actions", "settings_actions": "Actions",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "S'annoncer", "settings_sendAdvertisement": "S'annoncer",
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
"settings_advertisementSent": "Annonce envoyée", "settings_advertisementSent": "Annonce envoyée",
@ -2082,5 +2084,86 @@
"chat_sendMessage": "Envoyer un message", "chat_sendMessage": "Envoyer un message",
"room_guest": "Informations sur le serveur", "room_guest": "Informations sur le serveur",
"repeater_guest": "Informations sur les répéteurs", "repeater_guest": "Informations sur les répéteurs",
"settings_multiAck": "Plusieurs accusés de réception" "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}"
} }

@ -167,6 +167,8 @@
"settings_privacyModeEnabled": "Adatvédelem mód beállítva", "settings_privacyModeEnabled": "Adatvédelem mód beállítva",
"settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva", "settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva",
"settings_actions": "Tevékenységek", "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_sendAdvertisement": "Hirdetés küldése",
"settings_sendAdvertisementSubtitle": "A nyilvános megjelenés", "settings_sendAdvertisementSubtitle": "A nyilvános megjelenés",
"settings_advertisementSent": "Hirdetés elküldve", "settings_advertisementSent": "Hirdetés elküldve",
@ -2120,5 +2122,86 @@
"room_guest": "Szoba szerver információk", "room_guest": "Szoba szerver információk",
"chat_sendMessage": "Üzenet küldése", "chat_sendMessage": "Üzenet küldése",
"repeater_guest": "Adatok a repeaterről", "repeater_guest": "Adatok a repeaterről",
"settings_multiAck": "Többszörös visszaigazolások" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Modalità privacy abilitata", "settings_privacyModeEnabled": "Modalità privacy abilitata",
"settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_privacyModeDisabled": "Modalità privacy disabilitata",
"settings_actions": "Azioni", "settings_actions": "Azioni",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Invia Annuncio", "settings_sendAdvertisement": "Invia Annuncio",
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
"settings_advertisementSent": "Annuncio inviato", "settings_advertisementSent": "Annuncio inviato",
@ -2082,5 +2084,86 @@
"repeater_guestTools": "Strumenti per gli ospiti", "repeater_guestTools": "Strumenti per gli ospiti",
"chat_sendMessage": "Invia messaggio", "chat_sendMessage": "Invia messaggio",
"room_guest": "Informazioni sul server", "room_guest": "Informazioni sul server",
"settings_multiAck": "ACK multipli" "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})."
} }

@ -167,6 +167,8 @@
"settings_privacyModeEnabled": "プライバシーモードが有効になっています", "settings_privacyModeEnabled": "プライバシーモードが有効になっています",
"settings_privacyModeDisabled": "プライバシーモードは無効化されています", "settings_privacyModeDisabled": "プライバシーモードは無効化されています",
"settings_actions": "行動", "settings_actions": "行動",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "広告を送信する", "settings_sendAdvertisement": "広告を送信する",
"settings_sendAdvertisementSubtitle": "現在、放送での活動", "settings_sendAdvertisementSubtitle": "現在、放送での活動",
"settings_advertisementSent": "広告が送信されました", "settings_advertisementSent": "広告が送信されました",
@ -2120,5 +2122,86 @@
"chat_sendMessage": "メッセージを送信する", "chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報", "repeater_guest": "繰り返し送信に関する情報",
"repeater_guestTools": "ゲスト向けツール", "repeater_guestTools": "ゲスト向けツール",
"settings_multiAck": "複数のACK応答" "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}"
} }

@ -167,6 +167,8 @@
"settings_privacyModeEnabled": "개인 정보 보호 모드 활성화", "settings_privacyModeEnabled": "개인 정보 보호 모드 활성화",
"settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화", "settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화",
"settings_actions": "행동", "settings_actions": "행동",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "광고 전송", "settings_sendAdvertisement": "광고 전송",
"settings_sendAdvertisementSubtitle": "방송 활동", "settings_sendAdvertisementSubtitle": "방송 활동",
"settings_advertisementSent": "광고 전송", "settings_advertisementSent": "광고 전송",
@ -2120,5 +2122,86 @@
"chat_sendMessage": "메시지를 보내기", "chat_sendMessage": "메시지를 보내기",
"repeater_guest": "반복 장비 정보", "repeater_guest": "반복 장비 정보",
"room_guest": "서버 정보", "room_guest": "서버 정보",
"settings_multiAck": "다중 ACK" "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})."
} }

@ -202,6 +202,12 @@ abstract class AppLocalizations {
/// **'Close'** /// **'Close'**
String get common_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. /// No description provided for @common_edit.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -916,6 +922,18 @@ abstract class AppLocalizations {
/// **'Actions'** /// **'Actions'**
String get settings_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. /// No description provided for @settings_sendAdvertisement.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2044,6 +2062,12 @@ abstract class AppLocalizations {
/// **'Public'** /// **'Public'**
String get channels_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. /// No description provided for @channels_private.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2758,6 +2782,12 @@ abstract class AppLocalizations {
/// **'successes'** /// **'successes'**
String get chat_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. /// No description provided for @chat_removePath.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2920,6 +2950,18 @@ abstract class AppLocalizations {
/// **'Unread: {count}'** /// **'Unread: {count}'**
String chat_unread(int 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. /// No description provided for @chat_openLink.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3064,6 +3106,42 @@ abstract class AppLocalizations {
/// **'Flags'** /// **'Flags'**
String get map_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. /// No description provided for @map_shareMarkerHere.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3226,6 +3304,12 @@ abstract class AppLocalizations {
/// **'Shared pin'** /// **'Shared pin'**
String get map_sharedPin; 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. /// No description provided for @map_joinRoom.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -5736,6 +5820,47 @@ abstract class AppLocalizations {
/// **'Terrain'** /// **'Terrain'**
String get losLegendTerrain; 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. /// No description provided for @losFrequencyLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -6484,6 +6609,66 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'System language'** /// **'System language'**
String get translation_systemLanguage; 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 class _AppLocalizationsDelegate

@ -44,6 +44,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get common_close => 'Затвори'; String get common_close => 'Затвори';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Редактирай'; String get common_edit => 'Редактирай';
@ -445,6 +448,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get settings_actions => 'Действия'; String get settings_actions => 'Действия';
@override
String get settings_deleteAllPaths => 'Delete All Paths';
@override
String get settings_deleteAllPathsSubtitle =>
'Clear all path data from contacts.';
@override @override
String get settings_sendAdvertisement => 'Изпрати Реклама'; String get settings_sendAdvertisement => 'Изпрати Реклама';
@ -1095,6 +1105,11 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get channels_public => 'Публично'; String get channels_public => 'Публично';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Личен'; String get channels_private => 'Личен';
@ -1512,6 +1527,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get chat_successes => 'Успехи'; String get chat_successes => 'Успехи';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Премахни пътя'; String get chat_removePath => 'Премахни пътя';
@ -1614,6 +1632,12 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Непрочетени: $count'; return 'Непрочетени: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Отваряне на връзката?'; String get chat_openLink => 'Отваряне на връзката?';
@ -1695,6 +1719,24 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get map_flags => 'Флаг'; 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 @override
String get map_shareMarkerHere => 'Споделете маркер тук'; String get map_shareMarkerHere => 'Споделете маркер тук';
@ -1780,6 +1822,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get map_sharedPin => 'Споделено копие'; String get map_sharedPin => 'Споделено копие';
@override
String get map_sharedAt => 'Споделено';
@override @override
String get map_joinRoom => 'Присъедини се към стаята'; String get map_joinRoom => 'Присъедини се към стаята';
@ -3292,6 +3337,37 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Терен'; 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 @override
String get losFrequencyLabel => 'Честота'; String get losFrequencyLabel => 'Честота';
@ -3756,4 +3832,40 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Език на системата'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get common_close => 'Schließen'; String get common_close => 'Schließen';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Bearbeiten'; String get common_edit => 'Bearbeiten';
@ -443,6 +446,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings_actions => 'Aktionen'; 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 @override
String get settings_sendAdvertisement => 'Sende Ankündigung'; String get settings_sendAdvertisement => 'Sende Ankündigung';
@ -1090,6 +1100,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get channels_public => 'Öffentlich'; String get channels_public => 'Öffentlich';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privat'; String get channels_private => 'Privat';
@ -1511,6 +1526,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get chat_successes => 'Erfolgreich'; String get chat_successes => 'Erfolgreich';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Pfad entfernen'; String get chat_removePath => 'Pfad entfernen';
@ -1611,6 +1629,12 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Ungelesen: $count'; return 'Ungelesen: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Link öffnen?'; String get chat_openLink => 'Link öffnen?';
@ -1692,6 +1716,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get map_flags => 'Flags'; 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 @override
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.'; String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
@ -1777,6 +1819,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get map_sharedPin => 'Gemeinsames Passwort'; String get map_sharedPin => 'Gemeinsames Passwort';
@override
String get map_sharedAt => 'Geteilt';
@override @override
String get map_joinRoom => 'Beitreten Sie dem Raum'; String get map_joinRoom => 'Beitreten Sie dem Raum';
@ -3297,6 +3342,37 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Gelände'; 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 @override
String get losFrequencyLabel => 'Frequenz'; String get losFrequencyLabel => 'Frequenz';
@ -3767,4 +3843,40 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Sprache des Systems'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get common_close => 'Close'; String get common_close => 'Close';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Edit'; String get common_edit => 'Edit';
@ -435,6 +438,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_actions => 'Actions'; 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 @override
String get settings_sendAdvertisement => 'Send Advertisement'; String get settings_sendAdvertisement => 'Send Advertisement';
@ -1072,6 +1082,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get channels_public => 'Public'; String get channels_public => 'Public';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Private'; String get channels_private => 'Private';
@ -1482,6 +1497,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get chat_successes => 'successes'; String get chat_successes => 'successes';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Remove path'; String get chat_removePath => 'Remove path';
@ -1579,6 +1597,12 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Unread: $count'; return 'Unread: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Open Link?'; String get chat_openLink => 'Open Link?';
@ -1660,6 +1684,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_flags => 'Flags'; 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 @override
String get map_shareMarkerHere => 'Share marker here'; String get map_shareMarkerHere => 'Share marker here';
@ -1744,6 +1786,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_sharedPin => 'Shared pin'; String get map_sharedPin => 'Shared pin';
@override
String get map_sharedAt => 'Shared';
@override @override
String get map_joinRoom => 'Join Room'; String get map_joinRoom => 'Join Room';
@ -3234,6 +3279,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terrain'; 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 @override
String get losFrequencyLabel => 'Frequency'; String get losFrequencyLabel => 'Frequency';
@ -3685,4 +3761,40 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'System language'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get common_close => 'Cerrar'; String get common_close => 'Cerrar';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Editar'; String get common_edit => 'Editar';
@ -442,6 +445,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_actions => 'Acciones'; 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 @override
String get settings_sendAdvertisement => 'Enviar Anuncio'; String get settings_sendAdvertisement => 'Enviar Anuncio';
@ -1092,6 +1102,11 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get channels_public => 'Público'; String get channels_public => 'Público';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privado'; String get channels_private => 'Privado';
@ -1509,6 +1524,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get chat_successes => 'Éxitos'; String get chat_successes => 'Éxitos';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Eliminar ruta'; String get chat_removePath => 'Eliminar ruta';
@ -1610,6 +1628,12 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Sin leer: $count'; return 'Sin leer: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => '¿Abrir enlace?'; String get chat_openLink => '¿Abrir enlace?';
@ -1691,6 +1715,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get map_flags => 'Banderas'; 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 @override
String get map_shareMarkerHere => 'Compartir marcador aquí'; String get map_shareMarkerHere => 'Compartir marcador aquí';
@ -1776,6 +1818,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get map_sharedPin => 'Pin compartido'; String get map_sharedPin => 'Pin compartido';
@override
String get map_sharedAt => 'Compartido';
@override @override
String get map_joinRoom => 'Únete a la sala'; String get map_joinRoom => 'Únete a la sala';
@ -3291,6 +3336,37 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terreno'; 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 @override
String get losFrequencyLabel => 'Frecuencia'; String get losFrequencyLabel => 'Frecuencia';
@ -3760,4 +3836,40 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Idioma del sistema'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get common_close => 'Fermer'; String get common_close => 'Fermer';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Modifier'; String get common_edit => 'Modifier';
@ -447,6 +450,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get settings_actions => 'Actions'; 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 @override
String get settings_sendAdvertisement => 'S\'annoncer'; String get settings_sendAdvertisement => 'S\'annoncer';
@ -1097,6 +1107,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get channels_public => 'Public'; String get channels_public => 'Public';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privé'; String get channels_private => 'Privé';
@ -1516,6 +1531,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get chat_successes => 'Succès'; String get chat_successes => 'Succès';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Supprimer le chemin'; String get chat_removePath => 'Supprimer le chemin';
@ -1619,6 +1637,12 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Non lu : $count'; return 'Non lu : $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Ouvrir le lien ?'; String get chat_openLink => 'Ouvrir le lien ?';
@ -1701,6 +1725,24 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get map_flags => 'Drapeaux'; 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 @override
String get map_shareMarkerHere => 'Partager le marqueur ici'; String get map_shareMarkerHere => 'Partager le marqueur ici';
@ -1786,6 +1828,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get map_sharedPin => 'Clé partagée'; String get map_sharedPin => 'Clé partagée';
@override
String get map_sharedAt => 'Partagé';
@override @override
String get map_joinRoom => 'Rejoindre le room server'; String get map_joinRoom => 'Rejoindre le room server';
@ -3309,6 +3354,37 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terrain'; 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 @override
String get losFrequencyLabel => 'Fréquence'; String get losFrequencyLabel => 'Fréquence';
@ -3784,4 +3860,40 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Langue du système'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get common_close => 'Bezárás'; String get common_close => 'Bezárás';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Szerkesztés'; String get common_edit => 'Szerkesztés';
@ -445,6 +448,13 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get settings_actions => 'Tevékenységek'; 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 @override
String get settings_sendAdvertisement => 'Hirdetés küldése'; String get settings_sendAdvertisement => 'Hirdetés küldése';
@ -1097,6 +1107,11 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get channels_public => 'A nyilvánosság számára'; String get channels_public => 'A nyilvánosság számára';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Személyes'; String get channels_private => 'Személyes';
@ -1520,6 +1535,9 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get chat_successes => 'sikerek'; String get chat_successes => 'sikerek';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Törölje a elérési útvonalat'; String get chat_removePath => 'Törölje a elérési útvonalat';
@ -1620,6 +1638,12 @@ class AppLocalizationsHu extends AppLocalizations {
return 'Olvasatlan: $count'; return 'Olvasatlan: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Nyisd meg a linket?'; String get chat_openLink => 'Nyisd meg a linket?';
@ -1702,6 +1726,24 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get map_flags => 'Zászló'; 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 @override
String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt'; String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt';
@ -1788,6 +1830,9 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get map_sharedPin => 'Gemeinsames PIN-kód'; String get map_sharedPin => 'Gemeinsames PIN-kód';
@override
String get map_sharedAt => 'Megosztva';
@override @override
String get map_joinRoom => 'Csatlakozás a szobához'; String get map_joinRoom => 'Csatlakozás a szobához';
@ -3304,6 +3349,37 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terület'; 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 @override
String get losFrequencyLabel => 'Hatósság'; String get losFrequencyLabel => 'Hatósság';
@ -3775,4 +3851,40 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Rendszer nyelvé'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get common_close => 'Chiudi'; String get common_close => 'Chiudi';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Modifica'; String get common_edit => 'Modifica';
@ -445,6 +448,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_actions => 'Azioni'; 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 @override
String get settings_sendAdvertisement => 'Invia Annuncio'; String get settings_sendAdvertisement => 'Invia Annuncio';
@ -1093,6 +1103,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get channels_public => 'Pubblico'; String get channels_public => 'Pubblico';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privato'; String get channels_private => 'Privato';
@ -1511,6 +1526,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get chat_successes => 'successi'; String get chat_successes => 'successi';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Rimuovi percorso'; String get chat_removePath => 'Rimuovi percorso';
@ -1613,6 +1631,12 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Non letti: $count'; return 'Non letti: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Aprire il link?'; String get chat_openLink => 'Aprire il link?';
@ -1694,6 +1718,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_flags => 'Bandiere'; 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 @override
String get map_shareMarkerHere => 'Condividi marcatore qui'; String get map_shareMarkerHere => 'Condividi marcatore qui';
@ -1778,6 +1820,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_sharedPin => 'Condividi PIN'; String get map_sharedPin => 'Condividi PIN';
@override
String get map_sharedAt => 'Condiviso';
@override @override
String get map_joinRoom => 'Unisciti alla stanza'; String get map_joinRoom => 'Unisciti alla stanza';
@ -3295,6 +3340,37 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terreno'; 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 @override
String get losFrequencyLabel => 'Frequenza'; String get losFrequencyLabel => 'Frequenza';
@ -3764,4 +3840,40 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Lingua del sistema'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get common_close => '閉じる'; String get common_close => '閉じる';
@override
String get common_done => 'Done';
@override @override
String get common_edit => '編集'; String get common_edit => '編集';
@ -422,6 +425,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get settings_actions => '行動'; String get settings_actions => '行動';
@override
String get settings_deleteAllPaths => 'Delete All Paths';
@override
String get settings_deleteAllPathsSubtitle =>
'Clear all path data from contacts.';
@override @override
String get settings_sendAdvertisement => '広告を送信する'; String get settings_sendAdvertisement => '広告を送信する';
@ -1039,6 +1049,11 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get channels_public => '一般の人々'; String get channels_public => '一般の人々';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => '個人の'; String get channels_private => '個人の';
@ -1444,6 +1459,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get chat_successes => '成功事例'; String get chat_successes => '成功事例';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'パスを削除する'; String get chat_removePath => 'パスを削除する';
@ -1539,6 +1557,12 @@ class AppLocalizationsJa extends AppLocalizations {
return '未読: $count'; return '未読: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'リンクを開く?'; String get chat_openLink => 'リンクを開く?';
@ -1617,6 +1641,24 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get map_flags => ''; 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 @override
String get map_shareMarkerHere => 'この場所でシェア'; String get map_shareMarkerHere => 'この場所でシェア';
@ -1700,6 +1742,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get map_sharedPin => '共有パスワード'; String get map_sharedPin => '共有パスワード';
@override
String get map_sharedAt => '共有済み';
@override @override
String get map_joinRoom => '部屋に参加する'; String get map_joinRoom => '部屋に参加する';
@ -3142,6 +3187,36 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get losLegendTerrain => '地形'; 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 @override
String get losFrequencyLabel => '周波数'; String get losFrequencyLabel => '周波数';
@ -3578,4 +3653,40 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'システム言語'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get common_close => '닫기'; String get common_close => '닫기';
@override
String get common_done => 'Done';
@override @override
String get common_edit => '수정'; String get common_edit => '수정';
@ -422,6 +425,13 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get settings_actions => '행동'; String get settings_actions => '행동';
@override
String get settings_deleteAllPaths => 'Delete All Paths';
@override
String get settings_deleteAllPathsSubtitle =>
'Clear all path data from contacts.';
@override @override
String get settings_sendAdvertisement => '광고 전송'; String get settings_sendAdvertisement => '광고 전송';
@ -1034,6 +1044,11 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get channels_public => '대중의'; String get channels_public => '대중의';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => '사립'; String get channels_private => '사립';
@ -1440,6 +1455,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get chat_successes => '성공 사례'; String get chat_successes => '성공 사례';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => '경로 제거'; String get chat_removePath => '경로 제거';
@ -1535,6 +1553,12 @@ class AppLocalizationsKo extends AppLocalizations {
return '읽지 않음: $count'; return '읽지 않음: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => '링크를 열기?'; String get chat_openLink => '링크를 열기?';
@ -1613,6 +1637,24 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get map_flags => '깃발'; 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 @override
String get map_shareMarkerHere => '여기에서 마커 공유'; String get map_shareMarkerHere => '여기에서 마커 공유';
@ -1696,6 +1738,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get map_sharedPin => '공유 비밀번호'; String get map_sharedPin => '공유 비밀번호';
@override
String get map_sharedAt => '공유됨';
@override @override
String get map_joinRoom => '방에 참여'; String get map_joinRoom => '방에 참여';
@ -3142,6 +3187,36 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get losLegendTerrain => '지형'; 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 @override
String get losFrequencyLabel => '빈도'; String get losFrequencyLabel => '빈도';
@ -3577,4 +3652,40 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get translation_systemLanguage => '시스템 언어'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get common_close => 'Sluiten'; String get common_close => 'Sluiten';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Bewerken'; String get common_edit => 'Bewerken';
@ -440,6 +443,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get settings_actions => 'Acties'; 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 @override
String get settings_sendAdvertisement => 'Verzend Advertentie'; String get settings_sendAdvertisement => 'Verzend Advertentie';
@ -1082,6 +1092,11 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get channels_public => 'Openbaar'; String get channels_public => 'Openbaar';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privé'; String get channels_private => 'Privé';
@ -1497,6 +1512,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get chat_successes => 'Succesvol'; String get chat_successes => 'Succesvol';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Pad verwijderen'; String get chat_removePath => 'Pad verwijderen';
@ -1598,6 +1616,12 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Nieuw: $count'; return 'Nieuw: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Link openen?'; String get chat_openLink => 'Link openen?';
@ -1679,6 +1703,24 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_flags => 'Vlaggen'; 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 @override
String get map_shareMarkerHere => 'Deel marker hier'; String get map_shareMarkerHere => 'Deel marker hier';
@ -1764,6 +1806,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_sharedPin => 'Gedeelde pin'; String get map_sharedPin => 'Gedeelde pin';
@override
String get map_sharedAt => 'Gedeeld';
@override @override
String get map_joinRoom => 'Kamer Toetreden'; String get map_joinRoom => 'Kamer Toetreden';
@ -3275,6 +3320,37 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terrein'; 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 @override
String get losFrequencyLabel => 'Frequentie'; String get losFrequencyLabel => 'Frequentie';
@ -3739,4 +3815,40 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Taal van het systeem'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get common_close => 'Zamknij'; String get common_close => 'Zamknij';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Edytuj'; String get common_edit => 'Edytuj';
@ -448,6 +451,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get settings_actions => 'Działania'; 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 @override
String get settings_sendAdvertisement => 'Wyślij rozgłoszenie'; String get settings_sendAdvertisement => 'Wyślij rozgłoszenie';
@ -1102,6 +1112,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get channels_public => 'Publiczny'; String get channels_public => 'Publiczny';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Prywatny'; String get channels_private => 'Prywatny';
@ -1523,6 +1538,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get chat_successes => 'Sukcesy'; String get chat_successes => 'Sukcesy';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Usuń ścieżkę'; String get chat_removePath => 'Usuń ścieżkę';
@ -1624,6 +1642,12 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Nieprzeczytane: $count'; return 'Nieprzeczytane: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Otworzyć link?'; String get chat_openLink => 'Otworzyć link?';
@ -1705,6 +1729,24 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get map_flags => 'Flagi'; 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 @override
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
@ -1790,6 +1832,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get map_sharedPin => 'Udostępniona pinezka'; String get map_sharedPin => 'Udostępniona pinezka';
@override
String get map_sharedAt => 'Udostępnione';
@override @override
String get map_joinRoom => 'Dołącz do pokoju'; String get map_joinRoom => 'Dołącz do pokoju';
@ -3302,6 +3347,37 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Teren'; 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 @override
String get losFrequencyLabel => 'Częstotliwość'; String get losFrequencyLabel => 'Częstotliwość';
@ -3772,4 +3848,40 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Język systemu'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get common_close => 'Fechar'; String get common_close => 'Fechar';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Editar'; String get common_edit => 'Editar';
@ -444,6 +447,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get settings_actions => 'Ações'; 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 @override
String get settings_sendAdvertisement => 'Enviar Publicidade'; String get settings_sendAdvertisement => 'Enviar Publicidade';
@ -1093,6 +1103,11 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get channels_public => 'Público'; String get channels_public => 'Público';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privado'; String get channels_private => 'Privado';
@ -1508,6 +1523,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get chat_successes => 'Sucessos'; String get chat_successes => 'Sucessos';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Remover caminho'; String get chat_removePath => 'Remover caminho';
@ -1609,6 +1627,12 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Não lido: $count'; return 'Não lido: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Abrir link?'; String get chat_openLink => 'Abrir link?';
@ -1691,6 +1715,24 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get map_flags => 'Bandeiras'; 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 @override
String get map_shareMarkerHere => 'Compartilhar marcador aqui'; String get map_shareMarkerHere => 'Compartilhar marcador aqui';
@ -1776,6 +1818,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get map_sharedPin => 'Pin compartilhado'; String get map_sharedPin => 'Pin compartilhado';
@override
String get map_sharedAt => 'Compartilhado';
@override @override
String get map_joinRoom => 'Junte-se à Sala'; String get map_joinRoom => 'Junte-se à Sala';
@ -3289,6 +3334,37 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terreno'; 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 @override
String get losFrequencyLabel => 'Frequência'; String get losFrequencyLabel => 'Frequência';
@ -3753,4 +3829,40 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Idioma do sistema'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get common_close => 'Закрыть'; String get common_close => 'Закрыть';
@override
String get common_done => 'Готово';
@override @override
String get common_edit => 'Изменить'; String get common_edit => 'Изменить';
@ -444,6 +447,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get settings_actions => 'Действия'; String get settings_actions => 'Действия';
@override
String get settings_deleteAllPaths => 'Delete All Paths';
@override
String get settings_deleteAllPathsSubtitle =>
'Clear all path data from contacts.';
@override @override
String get settings_sendAdvertisement => 'Отправить анонсирование'; String get settings_sendAdvertisement => 'Отправить анонсирование';
@ -1093,6 +1103,11 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get channels_public => 'Публичный'; String get channels_public => 'Публичный';
@override
String channels_via(String path) {
return 'через $path';
}
@override @override
String get channels_private => 'Приватный'; String get channels_private => 'Приватный';
@ -1511,6 +1526,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get chat_successes => 'успешно'; String get chat_successes => 'успешно';
@override
String get chat_score => 'Оценка';
@override @override
String get chat_removePath => 'Удалить маршрут'; String get chat_removePath => 'Удалить маршрут';
@ -1613,6 +1631,12 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Непрочитанных: $count'; return 'Непрочитанных: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Открыть ссылку?'; String get chat_openLink => 'Открыть ссылку?';
@ -1694,6 +1718,24 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get map_flags => 'Флаги'; 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 @override
String get map_shareMarkerHere => 'Поделиться меткой здесь'; String get map_shareMarkerHere => 'Поделиться меткой здесь';
@ -1779,6 +1821,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get map_sharedPin => 'Общая метка'; String get map_sharedPin => 'Общая метка';
@override
String get map_sharedAt => 'Поделено';
@override @override
String get map_joinRoom => 'Присоединиться к комнате'; String get map_joinRoom => 'Присоединиться к комнате';
@ -3294,6 +3339,38 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Рельеф'; 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 @override
String get losFrequencyLabel => 'Частота'; String get losFrequencyLabel => 'Частота';
@ -3767,4 +3844,40 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Язык системы'; 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 => 'Неизвестно';
} }

@ -44,6 +44,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get common_close => 'Zavrieť'; String get common_close => 'Zavrieť';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Upraviť'; String get common_edit => 'Upraviť';
@ -439,6 +442,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_actions => 'Možné akcie'; 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 @override
String get settings_sendAdvertisement => 'Odoslať reklamu'; String get settings_sendAdvertisement => 'Odoslať reklamu';
@ -1082,6 +1092,11 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get channels_public => 'Veľké verejné'; String get channels_public => 'Veľké verejné';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Osobné'; String get channels_private => 'Osobné';
@ -1499,6 +1514,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get chat_successes => 'Úspechy'; String get chat_successes => 'Úspechy';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Odstrániť cestu'; String get chat_removePath => 'Odstrániť cestu';
@ -1600,6 +1618,12 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Nezriadené: $count'; return 'Nezriadené: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Otvoriť odkaz?'; String get chat_openLink => 'Otvoriť odkaz?';
@ -1681,6 +1705,24 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get map_flags => 'Zástavy'; 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 @override
String get map_shareMarkerHere => 'Zdieľte značku tu'; String get map_shareMarkerHere => 'Zdieľte značku tu';
@ -1766,6 +1808,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get map_sharedPin => 'Zdieľaný PIN'; String get map_sharedPin => 'Zdieľaný PIN';
@override
String get map_sharedAt => 'Zdieľané';
@override @override
String get map_joinRoom => 'Pripojiť miestnosť'; String get map_joinRoom => 'Pripojiť miestnosť';
@ -3270,6 +3315,37 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terén'; 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 @override
String get losFrequencyLabel => 'Frekvencia'; String get losFrequencyLabel => 'Frekvencia';
@ -3734,4 +3810,40 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Jazyk systému'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get common_close => 'Zapri'; String get common_close => 'Zapri';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Uredi'; String get common_edit => 'Uredi';
@ -438,6 +441,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get settings_actions => 'Akcije'; 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 @override
String get settings_sendAdvertisement => 'Pošlji Oglas'; String get settings_sendAdvertisement => 'Pošlji Oglas';
@ -1080,6 +1090,11 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get channels_public => 'Javni'; String get channels_public => 'Javni';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Zasebni'; String get channels_private => 'Zasebni';
@ -1496,6 +1511,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get chat_successes => 'Uspešni'; String get chat_successes => 'Uspešni';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Izbriši pot'; String get chat_removePath => 'Izbriši pot';
@ -1595,6 +1613,12 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Nerešeno: $count'; return 'Nerešeno: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Odpreti povezavo?'; String get chat_openLink => 'Odpreti povezavo?';
@ -1677,6 +1701,24 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_flags => 'Zapestnice'; 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 @override
String get map_shareMarkerHere => 'Delite točke tukaj.'; String get map_shareMarkerHere => 'Delite točke tukaj.';
@ -1761,6 +1803,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_sharedPin => 'Deljeno naslovno geslo'; String get map_sharedPin => 'Deljeno naslovno geslo';
@override
String get map_sharedAt => 'Deljeno';
@override @override
String get map_joinRoom => 'Pridružiti sobo'; String get map_joinRoom => 'Pridružiti sobo';
@ -3271,6 +3316,37 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Teren'; 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 @override
String get losFrequencyLabel => 'Frekvenca'; String get losFrequencyLabel => 'Frekvenca';
@ -3738,4 +3814,40 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Jezik sistema'; 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';
} }

@ -44,6 +44,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get common_close => 'Stänga'; String get common_close => 'Stänga';
@override
String get common_done => 'Done';
@override @override
String get common_edit => 'Redigera'; String get common_edit => 'Redigera';
@ -436,6 +439,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settings_actions => 'Åtgärder'; 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 @override
String get settings_sendAdvertisement => 'Skicka Annons'; String get settings_sendAdvertisement => 'Skicka Annons';
@ -1073,6 +1083,11 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get channels_public => 'Offentligt'; String get channels_public => 'Offentligt';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => 'Privat'; String get channels_private => 'Privat';
@ -1490,6 +1505,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get chat_successes => 'framgångar'; String get chat_successes => 'framgångar';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Ta bort sökväg'; String get chat_removePath => 'Ta bort sökväg';
@ -1588,6 +1606,12 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Olästa: $count'; return 'Olästa: $count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => 'Öppna länk?'; String get chat_openLink => 'Öppna länk?';
@ -1669,6 +1693,24 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get map_flags => 'Flaggor'; 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 @override
String get map_shareMarkerHere => 'Dela markeringen här'; String get map_shareMarkerHere => 'Dela markeringen här';
@ -1754,6 +1796,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get map_sharedPin => 'Delad PIN'; String get map_sharedPin => 'Delad PIN';
@override
String get map_sharedAt => 'Delad';
@override @override
String get map_joinRoom => 'Gå med i rum'; String get map_joinRoom => 'Gå med i rum';
@ -3252,6 +3297,37 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get losLegendTerrain => 'Terräng'; 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 @override
String get losFrequencyLabel => 'Frekvens'; String get losFrequencyLabel => 'Frekvens';
@ -3715,4 +3791,40 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get translation_systemLanguage => 'Språk för systemet'; 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';
} }

File diff suppressed because it is too large Load Diff

@ -44,6 +44,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get common_close => '关闭'; String get common_close => '关闭';
@override
String get common_done => 'Done';
@override @override
String get common_edit => '编辑'; String get common_edit => '编辑';
@ -416,6 +419,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_actions => '操作'; String get settings_actions => '操作';
@override
String get settings_deleteAllPaths => 'Delete All Paths';
@override
String get settings_deleteAllPathsSubtitle =>
'Clear all path data from contacts.';
@override @override
String get settings_sendAdvertisement => '发送广播'; String get settings_sendAdvertisement => '发送广播';
@ -1021,6 +1031,11 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get channels_public => '公共'; String get channels_public => '公共';
@override
String channels_via(String path) {
return 'via $path';
}
@override @override
String get channels_private => '私有'; String get channels_private => '私有';
@ -1420,6 +1435,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get chat_successes => '成功'; String get chat_successes => '成功';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => '移除路径'; String get chat_removePath => '移除路径';
@ -1507,6 +1525,12 @@ class AppLocalizationsZh extends AppLocalizations {
return '未读:$count'; return '未读:$count';
} }
@override
String get chat_markAsUnread => 'Mark as Unread';
@override
String get chat_newMessages => 'New messages';
@override @override
String get chat_openLink => '打开链接?'; String get chat_openLink => '打开链接?';
@ -1585,6 +1609,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_flags => '标志'; 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 @override
String get map_shareMarkerHere => '在此分享标记'; String get map_shareMarkerHere => '在此分享标记';
@ -1668,6 +1710,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_sharedPin => '共享标记'; String get map_sharedPin => '共享标记';
@override
String get map_sharedAt => '已分享';
@override @override
String get map_joinRoom => '加入房间'; String get map_joinRoom => '加入房间';
@ -3066,6 +3111,36 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get losLegendTerrain => '地形'; 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 @override
String get losFrequencyLabel => '频率'; String get losFrequencyLabel => '频率';
@ -3474,4 +3549,40 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get translation_systemLanguage => '系统语言'; 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';
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Privacy modus is ingeschakeld", "settings_privacyModeEnabled": "Privacy modus is ingeschakeld",
"settings_privacyModeDisabled": "Privacy modus is uitgeschakeld", "settings_privacyModeDisabled": "Privacy modus is uitgeschakeld",
"settings_actions": "Acties", "settings_actions": "Acties",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Verzend Advertentie", "settings_sendAdvertisement": "Verzend Advertentie",
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden", "settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
"settings_advertisementSent": "Advertentie verzonden", "settings_advertisementSent": "Advertentie verzonden",
@ -2082,5 +2084,86 @@
"room_guest": "Informatie over de server", "room_guest": "Informatie over de server",
"chat_sendMessage": "Verzend bericht", "chat_sendMessage": "Verzend bericht",
"repeater_guest": "Informatie over herhalingsapparatuur", "repeater_guest": "Informatie over herhalingsapparatuur",
"settings_multiAck": "Meerdere bevestigingen" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Tryb prywatności włączony", "settings_privacyModeEnabled": "Tryb prywatności włączony",
"settings_privacyModeDisabled": "Tryb prywatności wyłączony", "settings_privacyModeDisabled": "Tryb prywatności wyłączony",
"settings_actions": "Działania", "settings_actions": "Działania",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Wyślij rozgłoszenie", "settings_sendAdvertisement": "Wyślij rozgłoszenie",
"settings_sendAdvertisementSubtitle": "Nadaj obecność teraz", "settings_sendAdvertisementSubtitle": "Nadaj obecność teraz",
"settings_advertisementSent": "Rozgłoszenie wysłane", "settings_advertisementSent": "Rozgłoszenie wysłane",
@ -2120,5 +2122,86 @@
"repeater_guestTools": "Narzędzia dla gości", "repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego", "repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze", "room_guest": "Informacje o serwerze",
"settings_multiAck": "Wielokrotne potwierdzenia odbioru" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Modo de privacidade ativado", "settings_privacyModeEnabled": "Modo de privacidade ativado",
"settings_privacyModeDisabled": "Modo de privacidade desativado", "settings_privacyModeDisabled": "Modo de privacidade desativado",
"settings_actions": "Ações", "settings_actions": "Ações",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Enviar Publicidade", "settings_sendAdvertisement": "Enviar Publicidade",
"settings_sendAdvertisementSubtitle": "Presença de transmissão agora", "settings_sendAdvertisementSubtitle": "Presença de transmissão agora",
"settings_advertisementSent": "Anúncio enviado", "settings_advertisementSent": "Anúncio enviado",
@ -2082,5 +2084,86 @@
"room_guest": "Informações do Servidor", "room_guest": "Informações do Servidor",
"chat_sendMessage": "Enviar mensagem", "chat_sendMessage": "Enviar mensagem",
"repeater_guest": "Informações sobre repetidores", "repeater_guest": "Informações sobre repetidores",
"repeater_guestTools": "Ferramentas para hóspedes" "repeater_guestTools": "Ferramentas para hóspedes",
"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})."
} }

@ -81,6 +81,8 @@
"settings_privacyModeEnabled": "Режим конфиденциальности включен", "settings_privacyModeEnabled": "Режим конфиденциальности включен",
"settings_privacyModeDisabled": "Режим конфиденциальности выключен", "settings_privacyModeDisabled": "Режим конфиденциальности выключен",
"settings_actions": "Действия", "settings_actions": "Действия",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Отправить анонсирование", "settings_sendAdvertisement": "Отправить анонсирование",
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
"settings_advertisementSent": "Анонсирование отправлено", "settings_advertisementSent": "Анонсирование отправлено",
@ -1322,5 +1324,86 @@
"repeater_guest": "Информация о ретрансляторе", "repeater_guest": "Информация о ретрансляторе",
"room_guest": "Информация о сервере", "room_guest": "Информация о сервере",
"repeater_guestTools": "Инструменты для гостей", "repeater_guestTools": "Инструменты для гостей",
"settings_multiAck": "Несколько подтверждений" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Ochranný režim je povolený.", "settings_privacyModeEnabled": "Ochranný režim je povolený.",
"settings_privacyModeDisabled": "Ochranný režim je vypnutý", "settings_privacyModeDisabled": "Ochranný režim je vypnutý",
"settings_actions": "Možné akcie", "settings_actions": "Možné akcie",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Odoslať reklamu", "settings_sendAdvertisement": "Odoslať reklamu",
"settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.", "settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.",
"settings_advertisementSent": "Reklama odeslaná", "settings_advertisementSent": "Reklama odeslaná",
@ -2082,5 +2084,86 @@
"chat_sendMessage": "Odoslať správu", "chat_sendMessage": "Odoslať správu",
"repeater_guest": "Informácie o opakovači", "repeater_guest": "Informácie o opakovači",
"room_guest": "Informácie o serveri", "room_guest": "Informácie o serveri",
"repeater_guestTools": "Nástroje pre hostí" "repeater_guestTools": "Nástroje pre hostí",
"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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Privatni način je omogočen.", "settings_privacyModeEnabled": "Privatni način je omogočen.",
"settings_privacyModeDisabled": "Privatni način je onemogočen.", "settings_privacyModeDisabled": "Privatni način je onemogočen.",
"settings_actions": "Akcije", "settings_actions": "Akcije",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Pošlji Oglas", "settings_sendAdvertisement": "Pošlji Oglas",
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah", "settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
"settings_advertisementSent": "Oglas poslan", "settings_advertisementSent": "Oglas poslan",
@ -2082,5 +2084,86 @@
"chat_sendMessage": "Pošlji sporočilo", "chat_sendMessage": "Pošlji sporočilo",
"room_guest": "Informacije o strežniku", "room_guest": "Informacije o strežniku",
"repeater_guestTools": "Naložila za goste", "repeater_guestTools": "Naložila za goste",
"settings_multiAck": "Več potrdil" "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})."
} }

@ -104,6 +104,8 @@
"settings_privacyModeEnabled": "Privatläget är aktiverat", "settings_privacyModeEnabled": "Privatläget är aktiverat",
"settings_privacyModeDisabled": "Privatläge är avstängt", "settings_privacyModeDisabled": "Privatläge är avstängt",
"settings_actions": "Åtgärder", "settings_actions": "Åtgärder",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "Skicka Annons", "settings_sendAdvertisement": "Skicka Annons",
"settings_sendAdvertisementSubtitle": "Sändning finns nu", "settings_sendAdvertisementSubtitle": "Sändning finns nu",
"settings_advertisementSent": "Annons skickad", "settings_advertisementSent": "Annons skickad",
@ -2082,5 +2084,86 @@
"chat_sendMessage": "Skicka meddelande", "chat_sendMessage": "Skicka meddelande",
"repeater_guestTools": "Gästverktyg", "repeater_guestTools": "Gästverktyg",
"room_guest": "Information om servern", "room_guest": "Information om servern",
"settings_multiAck": "Flera bekräftelser" "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})."
} }

@ -1,5 +1,5 @@
{ {
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "channels_channelDeleteFailed": "Не вдалось видалити канал \"{name}\"",
"@channels_channelDeleteFailed": { "@channels_channelDeleteFailed": {
"placeholders": { "placeholders": {
"name": { "name": {
@ -18,6 +18,7 @@
"common_save": "Зберегти", "common_save": "Зберегти",
"common_delete": "Видалити", "common_delete": "Видалити",
"common_close": "Закрити", "common_close": "Закрити",
"common_done": "Готово",
"common_edit": "Редагувати", "common_edit": "Редагувати",
"common_add": "Додати", "common_add": "Додати",
"common_settings": "Налаштування", "common_settings": "Налаштування",
@ -26,7 +27,7 @@
"common_disconnected": "Відключено", "common_disconnected": "Відключено",
"common_create": "Створити", "common_create": "Створити",
"common_continue": "Продовжити", "common_continue": "Продовжити",
"common_share": "Поділитися", "common_share": "Поділитись",
"common_copy": "Копіювати", "common_copy": "Копіювати",
"common_retry": "Повторити", "common_retry": "Повторити",
"common_hide": "Приховати", "common_hide": "Приховати",
@ -81,7 +82,7 @@
"device_meshcore": "MeshCore", "device_meshcore": "MeshCore",
"settings_title": "Налаштування", "settings_title": "Налаштування",
"settings_deviceInfo": "Інформація про пристрій", "settings_deviceInfo": "Інформація про пристрій",
"settings_appSettings": "Налаштування програми", "settings_appSettings": "Налаштування застосунку",
"settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти",
"settings_nodeSettings": "Налаштування вузла", "settings_nodeSettings": "Налаштування вузла",
"settings_nodeName": "Ім'я вузла", "settings_nodeName": "Ім'я вузла",
@ -91,19 +92,21 @@
"settings_radioSettings": "Налаштування радіо", "settings_radioSettings": "Налаштування радіо",
"settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення",
"settings_radioSettingsUpdated": "Налаштування радіо оновлено", "settings_radioSettingsUpdated": "Налаштування радіо оновлено",
"settings_location": "Розташування", "settings_location": "Геопозиція",
"settings_locationSubtitle": "GPS координати", "settings_locationSubtitle": "GPS-координати",
"settings_locationUpdated": "Розташування оновлено", "settings_locationUpdated": "Геопозицію оновлено",
"settings_locationBothRequired": "Введіть широту та довготу.", "settings_locationBothRequired": "Введіть широту та довготу.",
"settings_locationInvalid": "Некоректна широта або довгота.", "settings_locationInvalid": "Некоректна широта або довгота.",
"settings_latitude": "Широта", "settings_latitude": "Широта",
"settings_longitude": "Довгота", "settings_longitude": "Довгота",
"settings_privacyMode": "Режим приватності", "settings_privacyMode": "Режим приватності",
"settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
"settings_privacyModeEnabled": "Режим приватності увімкнено", "settings_privacyModeEnabled": "Режим приватності увімкнено",
"settings_privacyModeDisabled": "Режим приватності вимкнено", "settings_privacyModeDisabled": "Режим приватності вимкнено",
"settings_actions": "Дії", "settings_actions": "Дії",
"settings_deleteAllPaths": "Видалити всі шляхи",
"settings_deleteAllPathsSubtitle": "Очистити всі дані шляхів у контактах.",
"settings_sendAdvertisement": "Оголосити себе", "settings_sendAdvertisement": "Оголосити себе",
"settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз",
"settings_advertisementSent": "Оголошення надіслано", "settings_advertisementSent": "Оголошення надіслано",
@ -118,9 +121,9 @@
"settings_debug": "Налагодження", "settings_debug": "Налагодження",
"settings_bleDebugLog": "Журнал налагодження BLE", "settings_bleDebugLog": "Журнал налагодження BLE",
"settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані",
"settings_appDebugLog": "Журнал налагодження програми", "settings_appDebugLog": "Журнал налагодження застосунку",
"settings_appDebugLogSubtitle": "Повідомлення налагодження програми", "settings_appDebugLogSubtitle": "Повідомлення налагодження застосунку",
"settings_about": "Про програму", "settings_about": "Про застосунок",
"settings_aboutVersion": "MeshCore Open v{version}", "settings_aboutVersion": "MeshCore Open v{version}",
"@settings_aboutVersion": { "@settings_aboutVersion": {
"placeholders": { "placeholders": {
@ -156,7 +159,7 @@
} }
} }
}, },
"appSettings_title": "Налаштування програми", "appSettings_title": "Налаштування застосунку",
"appSettings_appearance": "Вигляд", "appSettings_appearance": "Вигляд",
"appSettings_theme": "Тема", "appSettings_theme": "Тема",
"appSettings_themeSystem": "Системна", "appSettings_themeSystem": "Системна",
@ -194,9 +197,9 @@
"appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб",
"appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання",
"appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.",
"appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатись автоматично.",
"appSettings_autoRouteRotation": "Авторотація маршруту", "appSettings_autoRouteRotation": "Авторотація маршруту",
"appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «через всю мережу» (flood)",
"appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена",
"appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена",
"appSettings_battery": "Батарея", "appSettings_battery": "Батарея",
@ -251,10 +254,10 @@
} }
}, },
"appSettings_debugCard": "Налагодження", "appSettings_debugCard": "Налагодження",
"appSettings_appDebugLogging": "Логування налагодження програми", "appSettings_appDebugLogging": "Журналювання налагодження застосунку",
"appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження застосунку в журнал для усунення несправностей.",
"appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", "appSettings_appDebugLoggingEnabled": "Журналювання налагодження застосунку увімкнено",
"appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", "appSettings_appDebugLoggingDisabled": "Журналювання налагодження застосунку вимкнено.",
"contacts_title": "Контакти", "contacts_title": "Контакти",
"contacts_noContacts": "Контактів не знайдено.", "contacts_noContacts": "Контактів не знайдено.",
"contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.",
@ -338,8 +341,9 @@
} }
} }
}, },
"channels_hashtagChannel": "Канал з хештегом", "channels_hashtagChannel": "Хештег-канал",
"channels_public": "Публічний", "channels_public": "Публічний",
"channels_via": "через {path}",
"channels_private": "Приватний", "channels_private": "Приватний",
"channels_publicChannel": "Публічний канал", "channels_publicChannel": "Публічний канал",
"channels_privateChannel": "Приватний канал", "channels_privateChannel": "Приватний канал",
@ -371,7 +375,7 @@
"channels_pskHex": "PSK (Hex)", "channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK",
"channels_enterChannelName": "Будь ласка, введіть назву каналу", "channels_enterChannelName": "Будь ласка, введіть назву каналу",
"channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", "channels_pskMustBe32Hex": "PSK має складатись з 32 шістнадцяткових символів.",
"channels_channelAdded": "Канал «{name}» додано", "channels_channelAdded": "Канал «{name}» додано",
"@channels_channelAdded": { "@channels_channelAdded": {
"placeholders": { "placeholders": {
@ -438,7 +442,7 @@
} }
} }
}, },
"chat_location": "Розташування", "chat_location": "Геопозиція",
"chat_sendMessageTo": "Надіслати повідомлення {contactName}", "chat_sendMessageTo": "Надіслати повідомлення {contactName}",
"@chat_sendMessageTo": { "@chat_sendMessageTo": {
"placeholders": { "placeholders": {
@ -482,17 +486,17 @@
"gifPicker_searchHint": "Пошук GIF...", "gifPicker_searchHint": "Пошук GIF...",
"gifPicker_poweredBy": "На базі GIPHY", "gifPicker_poweredBy": "На базі GIPHY",
"gifPicker_noGifsFound": "GIF не знайдено", "gifPicker_noGifsFound": "GIF не знайдено",
"gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", "gifPicker_failedLoad": "Не вдалось завантажити GIF-файли",
"gifPicker_failedSearch": "Пошук GIF не вдався", "gifPicker_failedSearch": "Пошук GIF не вдався",
"gifPicker_noInternet": "Немає інтернет-з'єднання", "gifPicker_noInternet": "Немає інтернет-з'єднання",
"debugLog_appTitle": "Журнал налагодження програми", "debugLog_appTitle": "Журнал налагодження застосунку",
"debugLog_bleTitle": "Журнал налагодження BLE", "debugLog_bleTitle": "Журнал налагодження BLE",
"debugLog_copyLog": "Копіювати журнал", "debugLog_copyLog": "Копіювати журнал",
"debugLog_clearLog": "Очистити журнал", "debugLog_clearLog": "Очистити журнал",
"debugLog_copied": "Журнал налагодження скопійовано", "debugLog_copied": "Журнал налагодження скопійовано",
"debugLog_bleCopied": "Журнал BLE скопійовано", "debugLog_bleCopied": "Журнал BLE скопійовано",
"debugLog_noEntries": "Поки що немає записів журналу налагодження.", "debugLog_noEntries": "Поки що немає записів журналу налагодження.",
"debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", "debugLog_enableInSettings": "Увімкніть налагодження застосунку в налаштуваннях",
"debugLog_frames": "Кадри", "debugLog_frames": "Кадри",
"debugLog_rawLogRx": "Необроблений лог - RX", "debugLog_rawLogRx": "Необроблений лог - RX",
"debugLog_noBleActivity": "Поки що немає активності BLE.", "debugLog_noBleActivity": "Поки що немає активності BLE.",
@ -562,12 +566,12 @@
"chat_pathManagement": "Керування шляхами", "chat_pathManagement": "Керування шляхами",
"chat_routingMode": "Режим маршрутизації", "chat_routingMode": "Режим маршрутизації",
"chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)",
"chat_forceFloodMode": "Примусово на всю мережу", "chat_forceFloodMode": "Примусово через всю мережу",
"chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", "chat_recentAckPaths": "Підтверджені шляхи (натисніть, щоб використати):",
"chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.",
"chat_hopSingular": "Стрибок", "chat_hopSingular": "Перехід",
"chat_hopPlural": "стрибків", "chat_hopPlural": "переходів",
"chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", "chat_hopsCount": "{count} {count, plural, =1{перехід} few{переходи} many{переходів} other{переходів}}",
"@chat_hopsCount": { "@chat_hopsCount": {
"placeholders": { "placeholders": {
"count": { "count": {
@ -576,6 +580,7 @@
} }
}, },
"chat_successes": "Успішно", "chat_successes": "Успішно",
"chat_score": "Оцінка",
"chat_removePath": "Видалити шлях", "chat_removePath": "Видалити шлях",
"chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.",
"chat_pathActions": "Дії зі шляхом:", "chat_pathActions": "Дії зі шляхом:",
@ -584,11 +589,11 @@
"chat_clearPath": "Очистити шлях", "chat_clearPath": "Очистити шлях",
"chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні",
"chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.",
"chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі застосунку",
"chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", "chat_floodModeEnabled": "Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.",
"chat_fullPath": "Повний шлях", "chat_fullPath": "Повний шлях",
"chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.",
"chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{перехід} few{переходи} many{переходів} other{переходів}} - {status}",
"@chat_pathSetHops": { "@chat_pathSetHops": {
"placeholders": { "placeholders": {
"hopCount": { "hopCount": {
@ -606,9 +611,9 @@
"chat_path": "Шлях", "chat_path": "Шлях",
"chat_publicKey": "Відкритий ключ", "chat_publicKey": "Відкритий ключ",
"chat_compressOutgoingMessages": "Стискати вихідні повідомлення", "chat_compressOutgoingMessages": "Стискати вихідні повідомлення",
"chat_floodForced": "На всю мережу (примусово)", "chat_floodForced": "Через всю мережу (примусово)",
"chat_directForced": "Прямий (примусово)", "chat_directForced": "Напряму (примусово)",
"chat_hopsForced": "{count} стрибків (примусово)", "chat_hopsForced": "{count} переходів (примусово)",
"@chat_hopsForced": { "@chat_hopsForced": {
"placeholders": { "placeholders": {
"count": { "count": {
@ -616,9 +621,9 @@
} }
} }
}, },
"chat_floodAuto": "На всю мережу (авто)", "chat_floodAuto": "Через всю мережу (авто)",
"chat_direct": "Прямий", "chat_direct": "Напряму",
"chat_poiShared": "Точкою інтересу поділилися", "chat_poiShared": "Поділилися точкою інтересу",
"chat_unread": "Непрочитано: {count}", "chat_unread": "Непрочитано: {count}",
"@chat_unread": { "@chat_unread": {
"placeholders": { "placeholders": {
@ -630,7 +635,7 @@
"chat_openLink": "Відкрити посилання?", "chat_openLink": "Відкрити посилання?",
"chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?",
"chat_open": "Відкрити", "chat_open": "Відкрити",
"chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", "chat_couldNotOpenLink": "Не вдалось відкрити посилання: {url}",
"@chat_couldNotOpenLink": { "@chat_couldNotOpenLink": {
"placeholders": { "placeholders": {
"url": { "url": {
@ -640,8 +645,8 @@
}, },
"chat_invalidLink": "Невірний формат посилання", "chat_invalidLink": "Невірний формат посилання",
"map_title": "Карта вузлів", "map_title": "Карта вузлів",
"map_noNodesWithLocation": "Немає вузлів з даними про розташування", "map_noNodesWithLocation": "Немає вузлів з даними про геопозицію",
"map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", "map_nodesNeedGps": "Вузли мають надавати свої GPS координати,\nщоб з'явитись на карті.",
"map_nodesCount": "Вузли: {count}", "map_nodesCount": "Вузли: {count}",
"@map_nodesCount": { "@map_nodesCount": {
"placeholders": { "placeholders": {
@ -666,19 +671,25 @@
"map_pinPrivate": "Замок (Приватний)", "map_pinPrivate": "Замок (Приватний)",
"map_pinPublic": "Ключ (Публічний)", "map_pinPublic": "Ключ (Публічний)",
"map_lastSeen": "Останній раз бачили", "map_lastSeen": "Останній раз бачили",
"map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", "map_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
"map_from": "Від", "map_from": "Від",
"map_source": "Джерело", "map_source": "Джерело",
"map_flags": "Прапорці", "map_flags": "Прапорці",
"map_shareMarkerHere": "Поділитися маркером тут", "map_type": "Тип",
"map_path": "Шлях",
"map_location": "Геопозиція",
"map_estLocation": "Орієнтовна геопозиція",
"map_publicKey": "Публічний ключ",
"map_publicKeyPrefixHint": "напр. ab12",
"map_shareMarkerHere": "Поділитись маркером тут",
"map_pinLabel": "Мітка піна", "map_pinLabel": "Мітка піна",
"map_label": "Мітка", "map_label": "Мітка",
"map_pointOfInterest": "Точка інтересу", "map_pointOfInterest": "Точка інтересу",
"map_sendToContact": "Надіслати контакту", "map_sendToContact": "Надіслати контакту",
"map_sendToChannel": "Надіслати в канал", "map_sendToChannel": "Надіслати в канал",
"map_noChannelsAvailable": "Немає доступних каналів", "map_noChannelsAvailable": "Немає доступних каналів",
"map_publicLocationShare": "Поділитися в публічному місці", "map_publicLocationShare": "Поділитись в публічному місці",
"map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", "map_publicLocationShareConfirm": "Ви збираєтесь поділитись геопозицією у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.",
"@map_publicLocationShareConfirm": { "@map_publicLocationShareConfirm": {
"placeholders": { "placeholders": {
"channelLabel": { "channelLabel": {
@ -686,7 +697,7 @@
} }
} }
}, },
"map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитись маркерами",
"map_filterNodes": "Фільтрувати вузли", "map_filterNodes": "Фільтрувати вузли",
"map_nodeTypes": "Типи вузлів", "map_nodeTypes": "Типи вузлів",
"map_chatNodes": "Вузли чату", "map_chatNodes": "Вузли чату",
@ -699,7 +710,7 @@
"map_showSharedMarkers": "Показувати спільні маркери", "map_showSharedMarkers": "Показувати спільні маркери",
"map_lastSeenTime": "Час останньої активності", "map_lastSeenTime": "Час останньої активності",
"map_sharedPin": "Спільний пін", "map_sharedPin": "Спільний пін",
"map_joinRoom": "Приєднатися до кімнати", "map_joinRoom": "Приєднатись до кімнати",
"map_manageRepeater": "Керувати ретранслятором", "map_manageRepeater": "Керувати ретранслятором",
"mapCache_title": "Офлайн-кеш карти", "mapCache_title": "Офлайн-кеш карти",
"mapCache_selectAreaFirst": "Спершу виберіть область для кешування", "mapCache_selectAreaFirst": "Спершу виберіть область для кешування",
@ -822,7 +833,7 @@
"time_minutes": "хвилин", "time_minutes": "хвилин",
"time_allTime": "Весь час", "time_allTime": "Весь час",
"dialog_disconnect": "Відключити", "dialog_disconnect": "Відключити",
"dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
"login_repeaterLogin": "Вхід у ретранслятор", "login_repeaterLogin": "Вхід у ретранслятор",
"login_roomLogin": "Вхід у кімнату", "login_roomLogin": "Вхід у кімнату",
"login_password": "Пароль", "login_password": "Пароль",
@ -833,8 +844,8 @@
"login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.",
"login_routing": "Маршрутизація", "login_routing": "Маршрутизація",
"login_routingMode": "Режим маршрутизації", "login_routingMode": "Режим маршрутизації",
"login_autoUseSavedPath": "Авто (використовувати збережений шлях)", "login_autoUseSavedPath": "Авто (збережений шлях)",
"login_forceFloodMode": "Примусово на всю мережу", "login_forceFloodMode": "Примусово через всю мережу",
"login_managePaths": "Керувати шляхами", "login_managePaths": "Керувати шляхами",
"login_login": "Вхід", "login_login": "Вхід",
"login_attempt": "Спроба {current}/{max}", "login_attempt": "Спроба {current}/{max}",
@ -867,7 +878,7 @@
} }
} }
}, },
"path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{переходом} few{переходами} many{переходами} other{переходами}}",
"@path_usingHopsPath": { "@path_usingHopsPath": {
"placeholders": { "placeholders": {
"count": { "count": {
@ -877,10 +888,10 @@
}, },
"path_enterCustomPath": "Ввести власний шлях", "path_enterCustomPath": "Ввести власний шлях",
"path_currentPathLabel": "Поточний шлях", "path_currentPathLabel": "Поточний шлях",
"path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.",
"path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).",
"path_labelHexPrefixes": "Hex-префікси", "path_labelHexPrefixes": "Hex-префікси",
"path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", "path_helperMaxHops": "Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)",
"path_selectFromContacts": "Вибрати з контактів:", "path_selectFromContacts": "Вибрати з контактів:",
"path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.",
"path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.",
@ -892,7 +903,7 @@
} }
} }
}, },
"path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", "path_tooLong": "Шлях занадто довгий. Максимум 64 переходи.",
"path_setPath": "Встановити шлях", "path_setPath": "Встановити шлях",
"repeater_management": "Керування ретранслятором", "repeater_management": "Керування ретранслятором",
"repeater_managementTools": "Інструменти керування", "repeater_managementTools": "Інструменти керування",
@ -907,7 +918,7 @@
"repeater_statusTitle": "Статус ретранслятора", "repeater_statusTitle": "Статус ретранслятора",
"repeater_routingMode": "Режим маршрутизації", "repeater_routingMode": "Режим маршрутизації",
"repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)",
"repeater_forceFloodMode": "Примусово на всю мережу", "repeater_forceFloodMode": "Примусово через всю мережу",
"repeater_pathManagement": "Керування шляхами", "repeater_pathManagement": "Керування шляхами",
"repeater_refresh": "Оновити", "repeater_refresh": "Оновити",
"repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.",
@ -952,7 +963,7 @@
} }
} }
}, },
"repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "repeater_packetTxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
"@repeater_packetTxTotal": { "@repeater_packetTxTotal": {
"placeholders": { "placeholders": {
"total": { "total": {
@ -966,7 +977,7 @@
} }
} }
}, },
"repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "repeater_packetRxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
"@repeater_packetRxTotal": { "@repeater_packetRxTotal": {
"placeholders": { "placeholders": {
"total": { "total": {
@ -980,7 +991,7 @@
} }
} }
}, },
"repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", "repeater_duplicatesFloodDirect": "Через всю мережу: {flood}, Прямі: {direct}",
"@repeater_duplicatesFloodDirect": { "@repeater_duplicatesFloodDirect": {
"placeholders": { "placeholders": {
"flood": { "flood": {
@ -1015,7 +1026,7 @@
"repeater_bandwidth": "Смуга пропускання", "repeater_bandwidth": "Смуга пропускання",
"repeater_spreadingFactor": "Коефіцієнт розширення", "repeater_spreadingFactor": "Коефіцієнт розширення",
"repeater_codingRate": "Швидкість кодування", "repeater_codingRate": "Швидкість кодування",
"repeater_locationSettings": "Налаштування розташування", "repeater_locationSettings": "Налаштування геопозиції",
"repeater_latitude": "Широта", "repeater_latitude": "Широта",
"repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)",
"repeater_longitude": "Довгота", "repeater_longitude": "Довгота",
@ -1026,9 +1037,9 @@
"repeater_guestAccess": "Гостьовий доступ", "repeater_guestAccess": "Гостьовий доступ",
"repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання",
"repeater_privacyMode": "Режим приватності", "repeater_privacyMode": "Режим приватності",
"repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", "repeater_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
"repeater_advertisementSettings": "Налаштування оголошень", "repeater_advertisementSettings": "Налаштування оголошень",
"repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", "repeater_localAdvertInterval": "Інтервал локальних оголошень (без ретрансляції)",
"repeater_localAdvertIntervalMinutes": "{minutes} хвилин", "repeater_localAdvertIntervalMinutes": "{minutes} хвилин",
"@repeater_localAdvertIntervalMinutes": { "@repeater_localAdvertIntervalMinutes": {
"placeholders": { "placeholders": {
@ -1037,7 +1048,7 @@
} }
} }
}, },
"repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", "repeater_floodAdvertInterval": "Інтервал оголошень через всю мережу (flood)",
"repeater_floodAdvertIntervalHours": "{hours} годин", "repeater_floodAdvertIntervalHours": "{hours} годин",
"@repeater_floodAdvertIntervalHours": { "@repeater_floodAdvertIntervalHours": {
"placeholders": { "placeholders": {
@ -1051,9 +1062,9 @@
"repeater_rebootRepeater": "Перезавантажити ретранслятор", "repeater_rebootRepeater": "Перезавантажити ретранслятор",
"repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора",
"repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?",
"repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентифікації",
"repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)",
"repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентифікацію для ретранслятора. Продовжити?",
"repeater_eraseFileSystem": "Очистити файлову систему", "repeater_eraseFileSystem": "Очистити файлову систему",
"repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора",
"repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!",
@ -1087,7 +1098,7 @@
"repeater_refreshBasicSettings": "Оновити основні налаштування", "repeater_refreshBasicSettings": "Оновити основні налаштування",
"repeater_refreshRadioSettings": "Оновити налаштування радіо", "repeater_refreshRadioSettings": "Оновити налаштування радіо",
"repeater_refreshTxPower": "Оновити потужність TX", "repeater_refreshTxPower": "Оновити потужність TX",
"repeater_refreshLocationSettings": "Оновити налаштування розташування", "repeater_refreshLocationSettings": "Оновити налаштування геопозиції",
"repeater_refreshPacketForwarding": "Оновити пересилання пакетів", "repeater_refreshPacketForwarding": "Оновити пересилання пакетів",
"repeater_refreshGuestAccess": "Оновити гостьовий доступ", "repeater_refreshGuestAccess": "Оновити гостьовий доступ",
"repeater_refreshPrivacyMode": "Оновити режим приватності", "repeater_refreshPrivacyMode": "Оновити режим приватності",
@ -1144,19 +1155,19 @@
"repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).",
"repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.",
"repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)",
"repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість переходів для вхідних пакетів flood (якщо >= max, пакет не пересилається).",
"repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.",
"repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.",
"repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.",
"repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.",
"repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення через всю мережу. Встановіть 0 для вимкнення.",
"repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)",
"repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.",
"repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)",
"repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)",
"repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.", "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.",
"repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.",
"repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «через всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).",
"repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.",
"repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.",
"repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.",
@ -1172,7 +1183,7 @@
"repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.",
"repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4",
"repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.",
"repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «через всю мережу» (flood).",
"repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.",
"repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»",
"repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.",
@ -1186,14 +1197,14 @@
"repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.",
"repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.",
"repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.", "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.",
"repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення геопозиції вузла:\n- none : не включати геопозицію в оголошення\n- share : ділитись геопозицією GPS (з SensorManager)\n- prefs : оголошувати геопозицію, збережену в налаштуваннях",
"repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення геопозиції.",
"repeater_commandsListTitle": "Список команд", "repeater_commandsListTitle": "Список команд",
"repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...",
"repeater_general": "Загальні", "repeater_general": "Загальні",
"repeater_settingsCategory": "Налаштування", "repeater_settingsCategory": "Налаштування",
"repeater_bridge": "Міст", "repeater_bridge": "Міст",
"repeater_logging": "Логування", "repeater_logging": "Журналювання",
"repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)",
"repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)",
"repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.",
@ -1264,7 +1275,7 @@
"channelPath_title": "Шлях пакету", "channelPath_title": "Шлях пакету",
"channelPath_viewMap": "Показати карту", "channelPath_viewMap": "Показати карту",
"channelPath_otherObservedPaths": "Інші спостережувані шляхи", "channelPath_otherObservedPaths": "Інші спостережувані шляхи",
"channelPath_repeaterHops": "Стрибки ретранслятора", "channelPath_repeaterHops": "Переходи через ретранслятори",
"channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.",
"channelPath_messageDetails": "Деталі повідомлення", "channelPath_messageDetails": "Деталі повідомлення",
"channelPath_senderLabel": "Відправник", "channelPath_senderLabel": "Відправник",
@ -1283,7 +1294,7 @@
} }
} }
}, },
"channelPath_noLocationData": "Немає даних про розташування", "channelPath_noLocationData": "Немає даних про геопозицію",
"channelPath_timeWithDate": "{day}/{month} {time}", "channelPath_timeWithDate": "{day}/{month} {time}",
"@channelPath_timeWithDate": { "@channelPath_timeWithDate": {
"placeholders": { "placeholders": {
@ -1307,9 +1318,9 @@
} }
}, },
"channelPath_unknownPath": "Невідомий", "channelPath_unknownPath": "Невідомий",
"channelPath_floodPath": "На всю мережу", "channelPath_floodPath": "Через всю мережу",
"channelPath_directPath": "Прямий", "channelPath_directPath": "Напряму",
"channelPath_observedZeroOf": "0 з {total} стрибків", "channelPath_observedZeroOf": "0 з {total} переходів",
"@channelPath_observedZeroOf": { "@channelPath_observedZeroOf": {
"placeholders": { "placeholders": {
"total": { "total": {
@ -1317,7 +1328,7 @@
} }
} }
}, },
"channelPath_observedSomeOf": "{observed} з {total} стрибків", "channelPath_observedSomeOf": "{observed} з {total} переходів",
"@channelPath_observedSomeOf": { "@channelPath_observedSomeOf": {
"placeholders": { "placeholders": {
"observed": { "observed": {
@ -1358,12 +1369,12 @@
} }
} }
}, },
"channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", "channelPath_noHopDetailsAvailable": "Деталі переходів недоступні для цього пакету.",
"channelPath_unknownRepeater": "Невідомий ретранслятор", "channelPath_unknownRepeater": "Невідомий ретранслятор",
"listFilter_tooltip": "Фільтр та сортування", "listFilter_tooltip": "Фільтр та сортування",
"listFilter_sortBy": "Сортувати за", "listFilter_sortBy": "Сортувати за",
"listFilter_latestMessages": "Останні повідомлення", "listFilter_latestMessages": "Останні повідомлення",
"listFilter_heardRecently": "Нещодавно чули", "listFilter_heardRecently": "Нещодавно почуті",
"listFilter_az": "А-Я", "listFilter_az": "А-Я",
"listFilter_filters": "Фільтри", "listFilter_filters": "Фільтри",
"listFilter_all": "Все", "listFilter_all": "Все",
@ -1380,20 +1391,20 @@
} }
}, },
"repeater_neighbors": "Сусіди", "repeater_neighbors": "Сусіди",
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", "repeater_neighborsSubtitle": "Показати сусідів, доступних без ретрансляції.",
"neighbors_receivedData": "Дані сусідів отримано", "neighbors_receivedData": "Дані сусідів отримано",
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}", "neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди", "neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
"neighbors_noData": "Дані про сусідів недоступні.", "neighbors_noData": "Дані про сусідів недоступні.",
"channels_createPrivateChannelDesc": "Захищено секретним ключем.", "channels_createPrivateChannelDesc": "Захищено секретним ключем.",
"channels_joinPrivateChannel": "Приєднатися до приватного каналу", "channels_joinPrivateChannel": "Приєднатись до приватного каналу",
"channels_createPrivateChannel": "Створити приватний канал", "channels_createPrivateChannel": "Створити приватний канал",
"channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.",
"channels_joinPublicChannel": "Приєднатися до публічного каналу", "channels_joinPublicChannel": "Приєднатись до публічного каналу",
"channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", "channels_joinPublicChannelDesc": "Будь-хто може приєднатись до цього каналу.",
"channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", "channels_joinHashtagChannel": "Приєднатись до хештег-каналу",
"channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", "channels_joinHashtagChannelDesc": "Будь-хто може приєднатись до хештег-каналів.",
"channels_scanQrCode": "Сканувати QR-код", "channels_scanQrCode": "Сканувати QR-код",
"channels_scanQrCodeComingSoon": "Скоро буде", "channels_scanQrCodeComingSoon": "Скоро буде",
"channels_enterHashtag": "Введіть хештег", "channels_enterHashtag": "Введіть хештег",
@ -1415,7 +1426,7 @@
"neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}",
"neighbors_heardAgo": "Почуто: {time} тому", "neighbors_heardAgo": "Почуто: {time} тому",
"settings_locationGPSEnable": "Увімкнути GPS", "settings_locationGPSEnable": "Увімкнути GPS",
"settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення геопозиції через GPS.",
"settings_locationIntervalSec": "Інтервал для GPS (Секунди)", "settings_locationIntervalSec": "Інтервал для GPS (Секунди)",
"settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.", "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.",
"contacts_manageRoom": "Керувати сервером кімнати", "contacts_manageRoom": "Керувати сервером кімнати",
@ -1479,10 +1490,10 @@
"common_ok": "ОК", "common_ok": "ОК",
"community_title": "Спільнота", "community_title": "Спільнота",
"community_create": "Створити спільноту", "community_create": "Створити спільноту",
"community_createDesc": "Створити нову спільноту та поділитися через QR-код.", "community_createDesc": "Створити нову спільноту та поділитись через QR-код.",
"community_join": "Приєднатися", "community_join": "Приєднатись",
"community_joinTitle": "Приєднатися до спільноти", "community_joinTitle": "Приєднатись до спільноти",
"community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", "community_joinConfirmation": "Ви бажаєте приєднатись до спільноти «{name}»?",
"community_scanQr": "Сканувати QR спільноти", "community_scanQr": "Сканувати QR спільноти",
"community_scanInstructions": "Наведіть камеру на QR-код спільноти.", "community_scanInstructions": "Наведіть камеру на QR-код спільноти.",
"community_showQr": "Показати QR-код", "community_showQr": "Показати QR-код",
@ -1492,9 +1503,9 @@
"community_enterName": "Введіть назву спільноти", "community_enterName": "Введіть назву спільноти",
"community_created": "Спільноту «{name}» створено", "community_created": "Спільноту «{name}» створено",
"community_joined": "Приєднався до спільноти «{name}»", "community_joined": "Приєднався до спільноти «{name}»",
"community_qrTitle": "Поділитися спільнотою", "community_qrTitle": "Поділитись спільнотою",
"community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатись до {name}",
"community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", "community_hashtagPrivacyHint": "Хештег-канали спільноти доступні лише членам спільноти",
"community_invalidQrCode": "Недійсний QR-код спільноти", "community_invalidQrCode": "Недійсний QR-код спільноти",
"community_alreadyMember": "Вже учасник", "community_alreadyMember": "Вже учасник",
"community_alreadyMemberMessage": "Ви вже є учасником «{name}».", "community_alreadyMemberMessage": "Ви вже є учасником «{name}».",
@ -1515,10 +1526,10 @@
}, },
"community_deleted": "Спільноту «{name}» покинуто", "community_deleted": "Спільноту «{name}» покинуто",
"community_addHashtagChannel": "Додати хештег спільноти", "community_addHashtagChannel": "Додати хештег спільноти",
"community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", "community_addHashtagChannelDesc": "Додати хештег-канал для цієї спільноти",
"community_selectCommunity": "Вибрати спільноту", "community_selectCommunity": "Вибрати спільноту",
"community_regularHashtag": "Звичайний хештег", "community_regularHashtag": "Звичайний хештег",
"community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатись)",
"community_communityHashtag": "Хештег спільноти", "community_communityHashtag": "Хештег спільноти",
"community_communityHashtagDesc": "Ексклюзивно для членів спільноти", "community_communityHashtagDesc": "Ексклюзивно для членів спільноти",
"community_forCommunity": "Для {name}", "community_forCommunity": "Для {name}",
@ -1550,13 +1561,13 @@
} }
} }
}, },
"community_regenerateSecret": "Перегенерувати секрет", "community_regenerateSecret": "Перегенерувати секретний ключ",
"community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Усі учасники матимуть відсканувати новий QR-код, щоб продовжити спілкування.",
"community_regenerate": "Перегенерувати", "community_regenerate": "Перегенерувати",
"community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", "community_secretRegenerated": "Секретний ключ для «{name}» перегенеровано",
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити секретний ключ для «{name}»",
"community_updateSecret": "Оновити секрет", "community_updateSecret": "Оновити секретний ключ",
"community_secretUpdated": "Зміну секрету для «{name}» оновлено", "community_secretUpdated": "Секретний ключ для «{name}» оновлено",
"@contacts_pathTraceTo": { "@contacts_pathTraceTo": {
"placeholders": { "placeholders": {
"name": { "name": {
@ -1565,76 +1576,76 @@
} }
}, },
"pathTrace_you": "Ви", "pathTrace_you": "Ви",
"pathTrace_failed": "Відстеження шляху не вдалося.", "pathTrace_failed": "Відстеження шляху не вдалось.",
"pathTrace_notAvailable": "Трасування шляху недоступне.", "pathTrace_notAvailable": "Трасування шляху недоступне.",
"pathTrace_refreshTooltip": "Оновити Path Trace", "pathTrace_refreshTooltip": "Оновити трасування шляху",
"contacts_pathTrace": "Трасування шляхів", "contacts_pathTrace": "Трасування шляхів",
"contacts_ping": "Пінгувати", "contacts_ping": "Пінгувати",
"contacts_repeaterPathTrace": "Трасування шляху до повторювача", "contacts_repeaterPathTrace": "Трасування шляху до ретранслятора",
"contacts_repeaterPing": "Пінгувати повторювач", "contacts_repeaterPing": "Пінгувати ретранслятор",
"contacts_roomPathTrace": "Трасування шляху до серверу кімнати", "contacts_roomPathTrace": "Трасування шляху до серверу кімнати",
"contacts_roomPing": "Пінг сервера кімнати", "contacts_roomPing": "Пінг сервера кімнати",
"contacts_chatTraceRoute": "Трасування шляху", "contacts_chatTraceRoute": "Трасування шляху",
"contacts_pathTraceTo": "Відстежити маршрут до {name}", "contacts_pathTraceTo": "Відстежити маршрут до {name}",
"contacts_invalidAdvertFormat": "Недійсні контактні дані", "contacts_invalidAdvertFormat": "Недійсні контактні дані",
"contacts_contactImported": "Контакт було імпортовано.", "contacts_contactImported": "Контакт було імпортовано.",
"contacts_contactImportFailed": "Контакт не вдалося імпортувати", "contacts_contactImportFailed": "Контакт не вдалось імпортувати",
"contacts_zeroHopAdvert": "Реклама без перехоплення", "contacts_zeroHopAdvert": "Оголошення без ретрансляції",
"contacts_floodAdvert": "Залив реклами", "contacts_floodAdvert": "Оголошення з ретрансляцією",
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_copyAdvertToClipboard": "Копіювати оголошення",
"contacts_clipboardEmpty": "Буфер обміну порожній", "contacts_clipboardEmpty": "Буфер обміну порожній",
"appSettings_languageRu": "Російська", "appSettings_languageRu": "Російська",
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
"contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_ShareContact": "Копіювати контакт у буфер обміну",
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", "contacts_zeroHopContactAdvertFailed": "Не вдалось надіслати контакт.",
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", "contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.",
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилось невдало",
"contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням",
"contacts_addContactFromClipboard": "Додати контакт з буфера обміну", "contacts_addContactFromClipboard": "Додати контакт з буфера",
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", "contacts_ShareContactZeroHop": "Поділитись контактом за оголошенням",
"notification_activityTitle": "Активність MeshCore", "notification_activityTitle": "Активність MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}",
"notification_channelMessagesCount": "{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_newTypeDiscovered": "Виявлено новий {contactType}",
"notification_receivedNewMessage": "Отримано нове повідомлення", "notification_receivedNewMessage": "Отримано нове повідомлення",
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", "settings_gpxExportRepeaters": "Експорт ретрансляторів і серверів кімнат у GPX",
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори та сервери кімнат з геопозицією у файл GPX.",
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
"settings_gpxExportNoContacts": "Немає контактів для експорту.", "settings_gpxExportNoContacts": "Немає контактів для експорту.",
"settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі",
"settings_gpxExportError": "Сталася помилка під час експорту.", "settings_gpxExportError": "Сталась помилка під час експорту.",
"settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", "settings_gpxExportAllSubtitle": "Експортує всі контакти з геопозицією у файл GPX.",
"settings_gpxExportAll": "Експортувати всі контакти до GPX", "settings_gpxExportAll": "Експорт усіх контактів у GPX",
"settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", "settings_gpxExportContactsSubtitle": "Експортує контакти з геопозицією у файл GPX.",
"settings_gpxExportContacts": "Експортувати супутників до GPX", "settings_gpxExportContacts": "Експорт контактів у GPX",
"settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", "settings_gpxExportRepeatersRoom": "Геопозиції ретрансляторів та серверів кімнат",
"settings_gpxExportChat": "Місця супутників", "settings_gpxExportChat": "Геопозиції контактів",
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
"settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportAllContacts": "Усі місця контактів",
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", "pathTrace_someHopsNoLocation": "Один або декілька переходів не мають даних про геопозицію!",
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
"map_runTrace": "Виконати трасування шляху", "map_runTrace": "Виконати трасування шляху",
"pathTrace_clearTooltip": "Очистити шлях", "pathTrace_clearTooltip": "Очистити шлях",
"map_removeLast": "Видалити останній", "map_removeLast": "Видалити останній",
"map_pathTraceCancelled": "Відмінується трасування шляху", "map_pathTraceCancelled": "Трасування шляху скасовано.",
"scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_enableBluetooth": "Увімкніть Bluetooth",
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
"scanner_chromeRequired": "Потрібен браузер Chrome", "scanner_chromeRequired": "Потрібен браузер Chrome",
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому вебзастосунку потрібен Google Chrome або браузер на базі Chromium.",
"scanner_bluetoothOff": "Bluetooth вимкнено", "scanner_bluetoothOff": "Bluetooth вимкнено",
"snrIndicator_lastSeen": "Останній раз бачили", "snrIndicator_lastSeen": "Останній раз бачили",
"snrIndicator_nearByRepeaters": "Ближні ретранслятори", "snrIndicator_nearByRepeaters": "Найближчі ретранслятори",
"chat_ShowAllPaths": "Показати всі шляхи", "chat_ShowAllPaths": "Показати всі шляхи",
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
"settings_clientRepeat": "Автономна система", "settings_clientRepeat": "Автономна система",
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "одиниці", "appSettings_unitsTitle": "Одиниці",
"appSettings_unitsMetric": "Метричний (м / км)", "appSettings_unitsMetric": "Метричні (м / км)",
"appSettings_unitsImperial": "Імперська (ft / mi)", "appSettings_unitsImperial": "Імперські (ft / mi)",
"map_lineOfSight": "Пряма видимість", "map_lineOfSight": "Пряма видимість",
"map_losScreenTitle": "Пряма видимість", "map_losScreenTitle": "Пряма видимість",
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
@ -1652,7 +1663,7 @@
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
"losShowDisplayNodes": "Показати вузли відображення", "losShowDisplayNodes": "Показати вузли відображення",
"losCustomPoints": "Користувальницькі точки", "losCustomPoints": "Користувальницькі точки",
"losCustomPointLabel": "Спеціальний {index}", "losCustomPointLabel": "Власна точка {index}",
"@losCustomPointLabel": { "@losCustomPointLabel": {
"placeholders": { "placeholders": {
"index": { "index": {
@ -1741,7 +1752,7 @@
}, },
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
"losRenameCustomPoint": "Перейменуйте спеціальну точку", "losRenameCustomPoint": "Перейменувати власну точку",
"losPointName": "Назва точки", "losPointName": "Назва точки",
"losShowPanelTooltip": "Показати панель LOS", "losShowPanelTooltip": "Показати панель LOS",
"losHidePanelTooltip": "Приховати панель LOS", "losHidePanelTooltip": "Приховати панель LOS",
@ -1821,19 +1832,19 @@
"contacts_unread": "Непрочитане", "contacts_unread": "Непрочитане",
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів", "settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
"settings_contactSettings": "Налаштування контактів", "settings_contactSettings": "Налаштування контактів",
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів", "contactsSettings_autoAddUsersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлених користувачів",
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі", "contactsSettings_autoAddRepeatersTitle": "Автоматично додавати ретранслятори",
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори", "contactsSettings_autoAddRepeatersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені ретранслятори",
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат", "contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами", "contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
"contactsSettings_autoAddTitle": "Автоматичне виявлення", "contactsSettings_autoAddTitle": "Автоматичне виявлення",
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів", "contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
"contactsSettings_title": "Налаштування контактів", "contactsSettings_title": "Налаштування контактів",
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.", "contactsSettings_autoAddRoomServersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сервери кімнат.",
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики", "contactsSettings_autoAddSensorsTitle": "Автоматично додавати сенсори",
"discoveredContacts_searchHint": "Знайти виявлені контакти", "discoveredContacts_searchHint": "Знайти виявлені контакти",
"discoveredContacts_contactAdded": "Контакт додано", "discoveredContacts_contactAdded": "Контакт додано",
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори", "contactsSettings_autoAddSensorsSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сенсори",
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше", "contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
"discoveredContacts_Title": "Виявлені контакти", "discoveredContacts_Title": "Виявлені контакти",
"discoveredContacts_noMatching": "Відповідних контактів не знайдено", "discoveredContacts_noMatching": "Відповідних контактів не знайдено",
@ -1844,9 +1855,9 @@
"common_deleteAll": "Видалити все", "common_deleteAll": "Видалити все",
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", "map_showGuessedLocations": "Показати геопозиції передбачених вузлів",
"map_guessedLocation": "Визначено місцезнаходження", "map_guessedLocation": "Передбачена геопозиція",
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", "usbScreenSubtitle": "Виберіть виявлений USB-пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
"usbScreenTitle": "Підключити через USB", "usbScreenTitle": "Підключити через USB",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
@ -1858,8 +1869,8 @@
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.", "usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.", "usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
"usbErrorNotConnected": "Немає підключених пристроїв USB.", "usbErrorNotConnected": "Немає підключених пристроїв USB.",
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.", "usbErrorOpenFailed": "Не вдалось відкрити вибране USB-пристрій.",
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.", "usbErrorConnectFailed": "Не вдалось підключитись до вибраного USB-пристрою.",
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.", "usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
@ -1874,9 +1885,9 @@
}, },
"usbStatus_searching": "Пошук пристроїв USB...", "usbStatus_searching": "Пошук пристроїв USB...",
"usbStatus_notConnected": "Виберіть пристрій USB", "usbStatus_notConnected": "Виберіть пристрій USB",
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", "usbConnectionFailed": "Не вдалось встановити з'єднання через USB: {error}",
"usbStatus_connecting": "Підключення до USB-пристрою...", "usbStatus_connecting": "Підключення до USB-пристрою...",
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", "usbErrorConnectTimedOut": "З'єднання не вдалось встановити. Переконайтесь, що пристрій має встановлене програмне забезпечення USB Companion.",
"@tcpStatus_connectingTo": { "@tcpStatus_connectingTo": {
"placeholders": { "placeholders": {
"endpoint": { "endpoint": {
@ -1894,18 +1905,18 @@
"connectionChoiceTcpLabel": "TCP", "connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10", "tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-адреса", "tcpHostLabel": "IP-адреса",
"tcpScreenTitle": "З'єднатися через протокол TCP", "tcpScreenTitle": "Підключитись через TCP",
"tcpPortLabel": "Порт", "tcpPortLabel": "Порт",
"tcpPortHint": "5000", "tcpPortHint": "5000",
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться", "tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
"tcpStatus_connectingTo": "Підключення до {endpoint}...", "tcpStatus_connectingTo": "Підключення до {endpoint}...",
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.", "tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.", "tcpErrorPortInvalid": "Порт має бути в межах від 1 до 65535.",
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.", "tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", "tcpErrorTimedOut": "З'єднання TCP завершилось через закінчення часу очікування.",
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}", "tcpConnectionFailed": "Не вдалось встановити з'єднання TCP: {error}",
"map_showDiscoveryContacts": "Показати контакти Відкриття", "map_showDiscoveryContacts": "Показати виявлені контакти",
"map_setAsMyLocation": "Встановити моє місцезнаходження", "map_setAsMyLocation": "Встановити мою геопозицію",
"@path_routeWeight": { "@path_routeWeight": {
"placeholders": { "placeholders": {
"weight": { "weight": {
@ -1916,12 +1927,12 @@
} }
} }
}, },
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися", "settings_privacySubtitle": "Керуйте інформацією, яка буде спільно використовуватись",
"settings_privacy": "Налаштування приватності", "settings_privacy": "Налаштування приватності",
"settings_telemetryBaseMode": "Режим базової телеметрії", "settings_telemetryBaseMode": "Режим базової телеметрії",
"settings_telemetryLocationMode": "Режим місця телеметрії", "settings_telemetryLocationMode": "Режим місця телеметрії",
"settings_advertLocation": "Розміщення реклами", "settings_advertLocation": "Геопозиція в оголошенні",
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення", "settings_advertLocationSubtitle": "Включити геопозицію в оголошення",
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.", "settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
"settings_allowAll": "Дозволити все", "settings_allowAll": "Дозволити все",
"settings_denyAll": "Відхилити все", "settings_denyAll": "Відхилити все",
@ -1929,8 +1940,8 @@
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії", "settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
"contact_info": "Контактна інформація", "contact_info": "Контактна інформація",
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії", "contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
"contact_teleLoc": "Розташування телеметрії", "contact_teleLoc": "Геопозиція телеметрії",
"contact_teleBase": "Базовий телебачення", "contact_teleBase": "Базова телеметрія",
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення", "contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
"contact_settings": "Налаштування контактів", "contact_settings": "Налаштування контактів",
"contact_telemetry": "Телеметрія", "contact_telemetry": "Телеметрія",
@ -1950,8 +1961,8 @@
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено", "settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"map_showOverlaps": "Перекриття ключа повторювача", "map_showOverlaps": "Перекриття ключів ретрансляторів",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом", "map_runTraceWithReturnPath": "Повернутись назад тим же шляхом",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
"placeholders": { "placeholders": {
"noiseDbm": { "noiseDbm": {
@ -1995,26 +2006,26 @@
} }
}, },
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.", "chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
"appSettings_languageHu": "Угорський", "appSettings_languageHu": "Угорська",
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.", "appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.",
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення", "appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
"appSettings_languageJa": "Японська", "appSettings_languageJa": "Японська",
"appSettings_languageKo": "Кореєська", "appSettings_languageKo": "Корейська",
"radioStats_tooltip": "Статистика радіо та мережі", "radioStats_tooltip": "Статистика радіо та мережі",
"radioStats_screenTitle": "Дані про радіостанції", "radioStats_screenTitle": "Статистика радіо",
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.", "radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіо.",
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.", "radioStats_firmwareTooOld": "Статистика радіо вимагає прошивки пристрою-компаньйона версії 8 або новішої.",
"radioStats_waiting": "Очікую на отримання даних…", "radioStats_waiting": "Очікування даних…",
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм", "radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм", "radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ", "radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд", "radioStats_txAir": "Час в ефірі TX (загальний): {seconds} секунд",
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд", "radioStats_rxAir": "Час в ефірі RX (загальний): {seconds} секунд",
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.", "radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм", "radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
"radioStats_stripWaiting": "Отримано статистику радіо…", "radioStats_stripWaiting": "Отримано статистику радіо…",
"radioStats_settingsTile": "Дані про радіостанції", "radioStats_settingsTile": "Статистика радіо",
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.", "radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час в ефірі.",
"@translation_downloadFailed": { "@translation_downloadFailed": {
"placeholders": { "placeholders": {
"error": { "error": {
@ -2028,9 +2039,9 @@
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.", "translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.", "translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
"translation_targetLanguage": "Цільова мова", "translation_targetLanguage": "Цільова мова",
"translation_useAppLanguage": "Використовуйте мову додатку", "translation_useAppLanguage": "Використовувати мову застосунку",
"translation_downloadedModelLabel": "Завантажений шаблон", "translation_downloadedModelLabel": "Завантажений шаблон",
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face", "translation_presetModelLabel": "Попередньо налаштована модель з Hugging Face",
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією", "translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
"translation_downloadModel": "Завантажити модель", "translation_downloadModel": "Завантажити модель",
"translation_downloading": "Завантаження...", "translation_downloading": "Завантаження...",
@ -2041,7 +2052,7 @@
"translation_deleteModel": "Видалити модель", "translation_deleteModel": "Видалити модель",
"translation_modelDownloaded": "Модель перекладу завантажена.", "translation_modelDownloaded": "Модель перекладу завантажена.",
"translation_downloadStopped": "Завантаження призупинено.", "translation_downloadStopped": "Завантаження призупинено.",
"translation_downloadFailed": "Не вдалося завантажити: {error}", "translation_downloadFailed": "Не вдалось завантажити: {error}",
"translation_enterUrlFirst": "Спочатку введіть URL моделі.", "translation_enterUrlFirst": "Спочатку введіть URL моделі.",
"@scanner_linuxPairingPinPrompt": { "@scanner_linuxPairingPinPrompt": {
"placeholders": { "placeholders": {
@ -2069,7 +2080,7 @@
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN", "scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника", "repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів", "repeater_cliQuickDiscovery": "Виявити сусідів",
"@repeater_clockSyncAfterLogin": { "@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login" "description": "Repeater setting: auto sync device clock after successful login"
}, },
@ -2078,9 +2089,60 @@
}, },
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.", "repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу", "repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
"repeater_guestTools": "Інструменти для гостей", "repeater_guestTools": "Гостьові інструменти",
"repeater_guest": "Інформація про ретранслятор", "repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати", "room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення", "chat_sendMessage": "Надіслати повідомлення",
"settings_multiAck": "Багато підтверджень" "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})."
} }

@ -109,6 +109,8 @@
"settings_privacyModeEnabled": "隐私模式已启用", "settings_privacyModeEnabled": "隐私模式已启用",
"settings_privacyModeDisabled": "隐私模式已关闭", "settings_privacyModeDisabled": "隐私模式已关闭",
"settings_actions": "操作", "settings_actions": "操作",
"settings_deleteAllPaths": "Delete All Paths",
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
"settings_sendAdvertisement": "发送广播", "settings_sendAdvertisement": "发送广播",
"settings_sendAdvertisementSubtitle": "立即发送广播", "settings_sendAdvertisementSubtitle": "立即发送广播",
"settings_advertisementSent": "已发送广播", "settings_advertisementSent": "已发送广播",
@ -2087,5 +2089,86 @@
"repeater_guestTools": "访客工具", "repeater_guestTools": "访客工具",
"repeater_guest": "重复器信息", "repeater_guest": "重复器信息",
"chat_sendMessage": "发送消息", "chat_sendMessage": "发送消息",
"room_guest": "服务器信息" "room_guest": "服务器信息",
"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})."
} }

@ -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);
}
}

@ -59,6 +59,9 @@ void main() async {
final notificationService = NotificationService(); final notificationService = NotificationService();
await notificationService.initialize(); await notificationService.initialize();
await backgroundService.initialize(); await backgroundService.initialize();
backgroundService.setLanguageOverrideProvider(
() => appSettingsService.settings.languageOverride,
);
_registerThirdPartyLicenses(); _registerThirdPartyLicenses();
await chatTextScaleService.initialize(); await chatTextScaleService.initialize();

@ -41,7 +41,10 @@ class Contact {
String get publicKeyHex => pubKeyToHex(publicKey); 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) { switch (type) {
case advTypeChat: case advTypeChat:
return 'Chat'; return 'Chat';
@ -56,17 +59,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 { bool get hasLocation {
const double epsilon = 1e-6; const double epsilon = 1e-6;
final lat = latitude ?? 0.0; final lat = latitude ?? 0.0;

@ -1240,15 +1240,20 @@ class AppSettingsScreen extends StatelessWidget {
if (!context.mounted) return; if (!context.mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
// TODO: l10n content: Text(
content: Text('Deleted ${translationModelFriendlyName(model)}.'), context.l10n.appSettings_translationModelDeleted(
translationModelFriendlyName(model),
),
),
); );
} catch (error) { } catch (error) {
if (!context.mounted) return; if (!context.mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text('Delete failed: $error'), content: Text(
); // TODO: l10n context.l10n.appSettings_translationModelDeleteFailed('$error'),
),
);
} }
} }

@ -4,7 +4,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
@ -33,13 +33,19 @@ import '../widgets/message_translation_button.dart';
import '../widgets/message_status_icon.dart'; import '../widgets/message_status_icon.dart';
import '../widgets/radio_stats_entry.dart'; import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart'; import '../widgets/translated_message_content.dart';
import '../widgets/unread_divider.dart';
import 'channel_message_path_screen.dart'; import 'channel_message_path_screen.dart';
import 'map_screen.dart'; import 'map_screen.dart';
class ChannelChatScreen extends StatefulWidget { class ChannelChatScreen extends StatefulWidget {
final Channel channel; final Channel channel;
final int initialUnreadCount;
const ChannelChatScreen({super.key, required this.channel}); const ChannelChatScreen({
super.key,
required this.channel,
this.initialUnreadCount = 0,
});
@override @override
State<ChannelChatScreen> createState() => _ChannelChatScreenState(); State<ChannelChatScreen> createState() => _ChannelChatScreenState();
@ -56,32 +62,46 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
MeshCoreConnector? _connector; MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt; DateTime? _lastChannelSendAt;
bool _channelSkipNextBottomSnap = false; bool _channelSkipNextBottomSnap = false;
String? _unreadDividerMessageId;
String? _cachedFormatLocale;
late DateFormat _hmFormat;
late DateFormat _mdFormat;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange); _textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages; _scrollController.onScrollNearTop = _loadOlderMessages;
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings; final settings = context.read<AppSettingsService>().settings;
final idx = widget.channel.index; final idx = widget.channel.index;
final unread = connector.getUnreadCountForChannelIndex(idx); final unread = widget.initialUnreadCount;
final messages = connector.getChannelMessages(widget.channel);
ChannelMessage? anchor; ChannelMessage? anchor;
if (settings.jumpToOldestUnread && unread > 0) { if (unread > 0) {
anchor = _findOldestUnreadChannelAnchor( anchor = _findOldestUnreadChannelAnchor(messages, unread);
connector.getChannelMessages(widget.channel),
unread,
);
} }
setState(() {
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
});
connector.setActiveChannel(idx); connector.setActiveChannel(idx);
_connector = connector; _connector = connector;
if (anchor != null) { if (anchor != null && settings.jumpToOldestUnread) {
_channelSkipNextBottomSnap = true; _channelSkipNextBottomSnap = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_scrollController.jumpToEstimatedOffset(
unreadCount: unread,
totalMessages: messages.length,
onJumped: () {
if (!mounted) return; if (!mounted) return;
_scrollToMessage(anchor!.messageId); _scrollToMessage(anchor!.messageId);
},
);
}); });
} }
}); });
@ -103,6 +123,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return oldest; return oldest;
} }
void _clearDividerAtBottom() {
if (!_scrollController.showJumpToBottom.value &&
_unreadDividerMessageId != null) {
setState(() => _unreadDividerMessageId = null);
}
}
void _onTextFieldFocusChange() { void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) { if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen(); _scrollController.handleKeyboardOpen();
@ -124,6 +151,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
@override @override
void dispose() { void dispose() {
_connector?.setActiveChannel(null); _connector?.setActiveChannel(null);
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
_textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.removeListener(_onTextFieldFocusChange);
_textFieldFocusNode.dispose(); _textFieldFocusNode.dispose();
_textController.dispose(); _textController.dispose();
@ -322,6 +350,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (!_messageKeys.containsKey(message.messageId)) { if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey(); _messageKeys[message.messageId] = GlobalKey();
} }
final isUnreadAnchor =
_unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId;
return Container( return Container(
key: _messageKeys[message.messageId]!, key: _messageKeys[message.messageId]!,
child: Builder( child: Builder(
@ -330,10 +361,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
.select<ChatTextScaleService, double>( .select<ChatTextScaleService, double>(
(service) => service.scale, (service) => service.scale,
); );
return _buildMessageBubble( final bubble = _buildMessageBubble(
message, message,
textScale, textScale,
); );
if (isUnreadAnchor) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [const UnreadDivider(), bubble],
);
}
return bubble;
}, },
), ),
); );
@ -353,12 +391,24 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
void _markAsUnread(ChannelMessage message) {
final connector = context.read<MeshCoreConnector>();
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) { Widget _buildMessageBubble(ChannelMessage message, double textScale) {
final settingsService = context.watch<AppSettingsService>(); final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final gifId = GifHelper.parseGif(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = parseMarkerText(message.text);
final translatedDisplayText = final translatedDisplayText =
message.translatedText != null && message.translatedText != null &&
message.translatedText!.trim().isNotEmpty message.translatedText!.trim().isNotEmpty
@ -446,6 +496,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
poi, poi,
isOutgoing, isOutgoing,
textScale, textScale,
message.senderName,
trailing: (!enableTracing && isOutgoing) trailing: (!enableTracing && isOutgoing)
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
@ -556,7 +607,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
? const EdgeInsets.symmetric(horizontal: 8) ? const EdgeInsets.symmetric(horizontal: 8)
: EdgeInsets.zero, : EdgeInsets.zero,
child: Text( child: Text(
'via ${_formatPathPrefixes(displayPath)}', context.l10n.channels_via(
_formatPathPrefixes(displayPath),
),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[600], color: Colors.grey[600],
@ -577,7 +630,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
_formatTime(message.timestamp), _formatTime(context, message.timestamp),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[600], color: Colors.grey[600],
@ -702,7 +755,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
final gifId = GifHelper.parseGif(replyText); final gifId = GifHelper.parseGif(replyText);
final poi = _parsePoiMessage(replyText); final poi = parseMarkerText(replyText);
Widget contentPreview; Widget contentPreview;
if (gifId != null) { if (gifId != null) {
@ -813,24 +866,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
_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( Widget _buildPoiMessage(
BuildContext context, BuildContext context,
_PoiInfo poi, MarkerPayload poi,
bool isOutgoing, bool isOutgoing,
double textScale, { double textScale,
String senderName, {
Widget? trailing, Widget? trailing,
}) { }) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@ -850,12 +891,22 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
onPressed: () { onPressed: () {
final selfName = context.read<MeshCoreConnector>().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( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MapScreen( builder: (context) => MapScreen(
highlightPosition: LatLng(poi.lat, poi.lon), highlightPosition: poi.position,
highlightLabel: poi.label, highlightLabel: poi.label,
highlightMarkerKey: key,
), ),
), ),
); );
@ -1240,14 +1291,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String _formatTime(DateTime time) { String _formatTime(BuildContext context, DateTime time) {
final now = DateTime.now(); final now = DateTime.now();
final diff = now.difference(time); 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) { if (diff.inDays > 0) {
return '${time.day}/${time.month} ${time.hour}:${time.minute.toString().padLeft(2, '0')}'; return '${_mdFormat.format(time)} $hm';
} else { } else {
return '${time.hour}:${time.minute.toString().padLeft(2, '0')}'; return hm;
} }
} }
@ -1303,6 +1361,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_copyMessageText(message.text); _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( ListTile(
leading: const Icon(Icons.delete_outline), leading: const Icon(Icons.delete_outline),
title: Text(context.l10n.common_delete), title: Text(context.l10n.common_delete),
@ -1522,11 +1589,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});
}

@ -492,13 +492,18 @@ class _ChannelsScreenState extends State<ChannelsScreen>
], ],
), ),
onTap: () async { onTap: () async {
final unread =
connector.getUnreadCountForChannelIndex(channel.index);
connector.markChannelRead(channel.index); connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50)); await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) { if (context.mounted) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChannelChatScreen(channel: channel), builder: (context) => ChannelChatScreen(
channel: channel,
initialUnreadCount: unread,
),
), ),
); );
} }
@ -1552,7 +1557,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
if (!context.mounted) return; if (!context.mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text('Failed to update channel: $e'), content: Text(context.l10n.channels_channelUpdateFailed('$e')),
); );
} }
}, },

@ -9,7 +9,6 @@ import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
@ -21,6 +20,7 @@ import '../helpers/gif_helper.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/path_history.dart'; import '../models/path_history.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
@ -45,12 +45,18 @@ import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import '../widgets/unread_divider.dart';
import 'telemetry_screen.dart'; import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
final Contact contact; final Contact contact;
final int initialUnreadCount;
const ChatScreen({super.key, required this.contact}); const ChatScreen({
super.key,
required this.contact,
this.initialUnreadCount = 0,
});
@override @override
State<ChatScreen> createState() => _ChatScreenState(); State<ChatScreen> createState() => _ChatScreenState();
@ -64,6 +70,7 @@ class _ChatScreenState extends State<ChatScreen> {
bool _isLoadingOlder = false; bool _isLoadingOlder = false;
MeshCoreConnector? _connector; MeshCoreConnector? _connector;
Message? _pendingUnreadScrollTarget; Message? _pendingUnreadScrollTarget;
String? _unreadDividerMessageId;
DateTime? _lastTextSendAt; DateTime? _lastTextSendAt;
@override @override
@ -71,34 +78,47 @@ class _ChatScreenState extends State<ChatScreen> {
super.initState(); super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange); _textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages; _scrollController.onScrollNearTop = _loadOlderMessages;
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings; final settings = context.read<AppSettingsService>().settings;
final keyHex = widget.contact.publicKeyHex; final keyHex = widget.contact.publicKeyHex;
final unread = connector.getUnreadCountForContactKey(keyHex); final unread = widget.initialUnreadCount;
final messages = connector.getMessages(widget.contact);
Message? anchor; Message? anchor;
if (settings.jumpToOldestUnread && unread > 0) { if (unread > 0) {
anchor = _findOldestUnreadAnchor( anchor = _findOldestUnreadAnchor(messages, unread);
connector.getMessages(widget.contact),
unread,
);
} }
setState(() {
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
if (anchor != null && settings.jumpToOldestUnread) {
_pendingUnreadScrollTarget = anchor;
}
});
connector.setActiveContact(keyHex); connector.setActiveContact(keyHex);
_connector = connector; _connector = connector;
if (anchor != null) { if (anchor != null && settings.jumpToOldestUnread) {
setState(() => _pendingUnreadScrollTarget = anchor);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_scrollController.jumpToEstimatedOffset(
unreadCount: unread,
totalMessages: messages.length,
onJumped: () async {
if (!mounted) return; if (!mounted) return;
final ctx = _unreadScrollKey.currentContext; final ctx = _unreadScrollKey.currentContext;
if (ctx != null) { if (ctx != null) {
Scrollable.ensureVisible( await Scrollable.ensureVisible(
ctx, ctx,
duration: const Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
alignment: 0.15, alignment: 0.15,
); );
} }
if (mounted) {
setState(() => _pendingUnreadScrollTarget = null); setState(() => _pendingUnreadScrollTarget = null);
}
},
);
}); });
} }
}); });
@ -117,6 +137,13 @@ class _ChatScreenState extends State<ChatScreen> {
return oldest; return oldest;
} }
void _clearDividerAtBottom() {
if (!_scrollController.showJumpToBottom.value &&
_unreadDividerMessageId != null) {
setState(() => _unreadDividerMessageId = null);
}
}
void _onTextFieldFocusChange() { void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) { if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen(); _scrollController.handleKeyboardOpen();
@ -138,6 +165,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
void dispose() { void dispose() {
_connector?.setActiveContact(null); _connector?.setActiveContact(null);
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
_textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.removeListener(_onTextFieldFocusChange);
_textFieldFocusNode.dispose(); _textFieldFocusNode.dispose();
_textController.dispose(); _textController.dispose();
@ -480,6 +508,7 @@ class _ChatScreenState extends State<ChatScreen> {
senderName: resolvedContact.type == advTypeRoom senderName: resolvedContact.type == advTypeRoom
? "${contact.name} [$fourByteHex]" ? "${contact.name} [$fourByteHex]"
: contact.name, : contact.name,
sourceId: widget.contact.publicKeyHex,
isRoomServer: resolvedContact.type == advTypeRoom, isRoomServer: resolvedContact.type == advTypeRoom,
textScale: textScale, textScale: textScale,
onTap: () => _openMessagePath(message, contact), onTap: () => _openMessagePath(message, contact),
@ -487,10 +516,19 @@ class _ChatScreenState extends State<ChatScreen> {
onRetryReaction: (msg, emoji) => onRetryReaction: (msg, emoji) =>
_sendReaction(msg, contact, 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)) { if (identical(message, _pendingUnreadScrollTarget)) {
return KeyedSubtree(key: _unreadScrollKey, child: bubble); return KeyedSubtree(key: _unreadScrollKey, child: child);
} }
return bubble; return child;
}, },
); );
}, },
@ -498,6 +536,18 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
void _markAsUnread(Message message) {
final connector = context.read<MeshCoreConnector>();
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) { Widget _buildInputBar(MeshCoreConnector connector) {
final maxBytes = maxContactMessageBytes(); final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@ -1184,8 +1234,8 @@ class _ChatScreenState extends State<ChatScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildInfoRow(context.l10n.chat_type, contact.typeLabel), _buildInfoRow(context.l10n.chat_type, contact.typeLabel(context.l10n)),
_buildInfoRow(context.l10n.chat_path, contact.pathLabel), _buildInfoRow(context.l10n.chat_path, contact.pathLabel(context.l10n)),
_buildInfoRow( _buildInfoRow(
context.l10n.contact_lastSeen, context.l10n.contact_lastSeen,
_formatContactLastMessage(contact.lastMessageAt), _formatContactLastMessage(contact.lastMessageAt),
@ -1409,11 +1459,15 @@ class _ChatScreenState extends State<ChatScreen> {
} }
void _openChat(BuildContext context, Contact contact) { void _openChat(BuildContext context, Contact contact) {
// Check if this is a repeater final connector = context.read<MeshCoreConnector>();
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex); final unread = connector.getUnreadCountForContactKey(contact.publicKeyHex);
connector.markContactRead(contact.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)), MaterialPageRoute(
builder: (context) =>
ChatScreen(contact: contact, initialUnreadCount: unread),
),
); );
} }
@ -1550,6 +1604,15 @@ class _ChatScreenState extends State<ChatScreen> {
_copyMessageText(message.text); _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( ListTile(
leading: const Icon(Icons.delete_outline), leading: const Icon(Icons.delete_outline),
title: Text(context.l10n.common_delete), title: Text(context.l10n.common_delete),
@ -1657,10 +1720,12 @@ class _MessageBubble extends StatelessWidget {
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final void Function(Message message, String emoji)? onRetryReaction; final void Function(Message message, String emoji)? onRetryReaction;
final double textScale; final double textScale;
final String sourceId;
const _MessageBubble({ const _MessageBubble({
required this.message, required this.message,
required this.senderName, required this.senderName,
required this.sourceId,
required this.isRoomServer, required this.isRoomServer,
required this.textScale, required this.textScale,
this.onTap, this.onTap,
@ -1675,7 +1740,7 @@ class _MessageBubble extends StatelessWidget {
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final gifId = GifHelper.parseGif(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = parseMarkerText(message.text);
final isFailed = message.status == MessageStatus.failed; final isFailed = message.status == MessageStatus.failed;
final bubbleColor = isFailed final bubbleColor = isFailed
? colorScheme.errorContainer ? colorScheme.errorContainer
@ -1767,6 +1832,7 @@ class _MessageBubble extends StatelessWidget {
textColor, textColor,
metaColor, metaColor,
textScale, textScale,
senderName,
trailing: (!enableTracing && isOutgoing) trailing: (!enableTracing && isOutgoing)
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
@ -1948,25 +2014,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( Widget _buildPoiMessage(
BuildContext context, BuildContext context,
_PoiInfo poi, MarkerPayload poi,
Color textColor, Color textColor,
Color metaColor, Color metaColor,
double textScale, { double textScale,
String senderName, {
Widget? trailing, Widget? trailing,
}) { }) {
return Row( return Row(
@ -1976,13 +2030,23 @@ class _MessageBubble extends StatelessWidget {
icon: Icon(Icons.location_on_outlined, color: textColor), icon: Icon(Icons.location_on_outlined, color: textColor),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
onPressed: () { onPressed: () async {
final selfName = context.read<MeshCoreConnector>().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( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MapScreen( builder: (context) => MapScreen(
highlightPosition: LatLng(poi.lat, poi.lon), highlightPosition: poi.position,
highlightLabel: poi.label, highlightLabel: poi.label,
highlightMarkerKey: key,
), ),
), ),
); );
@ -2163,11 +2227,3 @@ class _MessageBubble extends StatelessWidget {
return '$hour:$minute'; 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});
}

@ -13,6 +13,7 @@ import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../models/contact_group.dart'; import '../models/contact_group.dart';
import '../services/ui_view_state_service.dart'; import '../services/ui_view_state_service.dart';
import '../utils/contact_search.dart'; import '../utils/contact_search.dart';
@ -930,10 +931,18 @@ class _ContactsScreenState extends State<ContactsScreen>
} else if (contact.type == advTypeRoom) { } else if (contact.type == advTypeRoom) {
_showRoomLogin(context, contact, RoomLoginDestination.chat); _showRoomLogin(context, contact, RoomLoginDestination.chat);
} else { } else {
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex); final connector = context.read<MeshCoreConnector>();
final unread =
connector.getUnreadCountForContactKey(contact.publicKeyHex);
connector.markContactRead(contact.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)), MaterialPageRoute(
builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
),
); );
} }
} }
@ -988,7 +997,10 @@ class _ContactsScreenState extends State<ContactsScreen>
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password, isAdmin) { onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); final connector = context.read<MeshCoreConnector>();
final unread =
connector.getUnreadCountForContactKey(room.publicKeyHex);
connector.markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -999,7 +1011,10 @@ class _ContactsScreenState extends State<ContactsScreen>
password: password, password: password,
isAdmin: isAdmin, isAdmin: isAdmin,
) )
: ChatScreen(contact: room), : ChatScreen(
contact: room,
initialUnreadCount: unread,
),
), ),
); );
}, },
@ -1122,7 +1137,9 @@ class _ContactsScreenState extends State<ContactsScreen>
return CheckboxListTile( return CheckboxListTile(
value: isSelected, value: isSelected,
title: Text(contact.name), title: Text(contact.name),
subtitle: Text(contact.typeLabel), subtitle: Text(
contact.typeLabel(context.l10n),
),
onChanged: (value) { onChanged: (value) {
setDialogState(() { setDialogState(() {
if (value == true) { if (value == true) {
@ -1464,7 +1481,7 @@ class _ContactTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
contact.pathLabel, contact.pathLabel(context.l10n),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

@ -62,6 +62,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
bool _loading = false; bool _loading = false;
String? _error; String? _error;
LineOfSightPathResult? _result; LineOfSightPathResult? _result;
LineOfSightObstruction? _selectedObstruction;
LineOfSightEndpoint? _start; LineOfSightEndpoint? _start;
LineOfSightEndpoint? _end; LineOfSightEndpoint? _end;
final List<LineOfSightEndpoint> _customEndpoints = []; final List<LineOfSightEndpoint> _customEndpoints = [];
@ -111,6 +112,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
if (start == null || end == null) { if (start == null || end == null) {
setState(() { setState(() {
_result = null; _result = null;
_selectedObstruction = null;
_error = _errorSelectStartEnd; _error = _errorSelectStartEnd;
}); });
return; return;
@ -142,6 +144,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
} }
setState(() { setState(() {
_result = result; _result = result;
_selectedObstruction = _defaultObstructionFor(result);
}); });
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
@ -156,6 +159,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
} }
setState(() { setState(() {
_result = null; _result = null;
_selectedObstruction = null;
_error = context.l10n.losRunFailed(e.toString()); _error = context.l10n.losRunFailed(e.toString());
}); });
} finally { } finally {
@ -184,6 +188,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
void _selectFromMap(LineOfSightEndpoint endpoint) { void _selectFromMap(LineOfSightEndpoint endpoint) {
setState(() { setState(() {
_result = null; _result = null;
_selectedObstruction = null;
_error = null; _error = null;
if (_start == null || (_start != null && _end != null)) { if (_start == null || (_start != null && _end != null)) {
_start = endpoint; _start = endpoint;
@ -241,6 +246,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
_start = null; _start = null;
_end = null; _end = null;
_result = null; _result = null;
_selectedObstruction = null;
_error = _errorSelectStartEnd; _error = _errorSelectStartEnd;
}); });
} }
@ -251,6 +257,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
if (identical(_start, endpoint)) _start = null; if (identical(_start, endpoint)) _start = null;
if (identical(_end, endpoint)) _end = null; if (identical(_end, endpoint)) _end = null;
_result = null; _result = null;
_selectedObstruction = null;
}); });
} }
@ -377,7 +384,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
), ),
if (_result != null && _result!.segments.isNotEmpty) if (_result != null && _result!.segments.isNotEmpty)
PolylineLayer(polylines: _buildSegmentPolylines(_result!)), PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
MarkerLayer(markers: _buildMarkers(endpoints)), MarkerLayer(
markers: _buildMarkers(endpoints, _primaryObstructions()),
),
], ],
), ),
if (_showHud) if (_showHud)
@ -445,6 +454,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
); );
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz; final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
final kFactorUsed = segment?.usedKFactor; final kFactorUsed = segment?.usedKFactor;
final obstructions =
segment?.obstructions ?? const <LineOfSightObstruction>[];
final endpoints = _visibleEndpoints(); final endpoints = _visibleEndpoints();
final distanceUnit = isImperial ? 'mi' : 'km'; final distanceUnit = isImperial ? 'mi' : 'km';
final heightUnit = isImperial ? 'ft' : 'm'; final heightUnit = isImperial ? 'ft' : 'm';
@ -463,31 +474,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (segment != null) if (segment != null)
SizedBox( _buildProfileView(segment, distanceUnit, heightUnit, isImperial)
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,
),
),
)
else else
SizedBox( SizedBox(
height: 44, height: 44,
@ -519,6 +506,96 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
fontWeight: FontWeight.w600, 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), const SizedBox(height: 4),
if (displayFrequencyMHz != null) if (displayFrequencyMHz != null)
Padding( Padding(
@ -605,6 +682,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
_showDisplayNodes = value; _showDisplayNodes = value;
_sanitizeSelection(); _sanitizeSelection();
_result = null; _result = null;
_selectedObstruction = null;
}); });
}, },
), ),
@ -655,6 +733,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() { setState(() {
_start = value; _start = value;
_result = null; _result = null;
_selectedObstruction = null;
}); });
if (_start != null && _end != null) { if (_start != null && _end != null) {
_runLos(); _runLos();
@ -670,6 +749,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() { setState(() {
_end = value; _end = value;
_result = null; _result = null;
_selectedObstruction = null;
}); });
if (_start != null && _end != null) { if (_start != null && _end != null) {
_runLos(); _runLos();
@ -769,6 +849,179 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
return _result!.segments.first.result; return _result!.segments.first.result;
} }
List<LineOfSightObstruction> _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) { String _profileStats(LineOfSightResult result, bool isImperial) {
final distance = isImperial final distance = isImperial
? (result.totalDistanceMeters / 1000.0) * _kmToMiles ? (result.totalDistanceMeters / 1000.0) * _kmToMiles
@ -820,8 +1073,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
return polylines; return polylines;
} }
List<Marker> _buildMarkers(List<LineOfSightEndpoint> endpoints) { List<Marker> _buildMarkers(
List<LineOfSightEndpoint> endpoints,
List<LineOfSightObstruction> obstructions,
) {
return [ 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) for (final endpoint in endpoints)
Marker( Marker(
point: endpoint.point, point: endpoint.point,
@ -1010,6 +1306,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
} }
} }
class _LosProfileGeometry {
static const horizontalPadding = 12.0;
static const verticalPadding = 12.0;
final List<LineOfSightSample> 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 { class _LosProfilePainter extends CustomPainter {
final List<LineOfSightSample> samples; final List<LineOfSightSample> samples;
final String distanceUnit; final String distanceUnit;
@ -1018,6 +1359,7 @@ class _LosProfilePainter extends CustomPainter {
final String terrainLabel; final String terrainLabel;
final String losBeamLabel; final String losBeamLabel;
final String radioHorizonLabel; final String radioHorizonLabel;
final int? selectedSampleIndex;
const _LosProfilePainter({ const _LosProfilePainter({
required this.samples, required this.samples,
@ -1027,6 +1369,7 @@ class _LosProfilePainter extends CustomPainter {
required this.terrainLabel, required this.terrainLabel,
required this.losBeamLabel, required this.losBeamLabel,
required this.radioHorizonLabel, required this.radioHorizonLabel,
this.selectedSampleIndex,
}); });
@override @override
@ -1212,6 +1555,32 @@ class _LosProfilePainter extends CustomPainter {
..color = horizonFillColor ..color = horizonFillColor
..style = PaintingStyle.fill, ..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 @override
@ -1222,7 +1591,8 @@ class _LosProfilePainter extends CustomPainter {
oldDelegate.badgeTextStyle != badgeTextStyle || oldDelegate.badgeTextStyle != badgeTextStyle ||
oldDelegate.terrainLabel != terrainLabel || oldDelegate.terrainLabel != terrainLabel ||
oldDelegate.losBeamLabel != losBeamLabel || oldDelegate.losBeamLabel != losBeamLabel ||
oldDelegate.radioHorizonLabel != radioHorizonLabel; oldDelegate.radioHorizonLabel != radioHorizonLabel ||
oldDelegate.selectedSampleIndex != selectedSampleIndex;
} }
void _drawUnitBadge(Canvas canvas, Size size) { void _drawUnitBadge(Canvas canvas, Size size) {

@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../services/map_marker_service.dart'; import '../services/map_marker_service.dart';
@ -37,6 +38,7 @@ import 'line_of_sight_map_screen.dart';
class MapScreen extends StatefulWidget { class MapScreen extends StatefulWidget {
final LatLng? highlightPosition; final LatLng? highlightPosition;
final String? highlightLabel; final String? highlightLabel;
final String? highlightMarkerKey;
final double highlightZoom; final double highlightZoom;
final bool hideBackButton; final bool hideBackButton;
@ -44,6 +46,7 @@ class MapScreen extends StatefulWidget {
super.key, super.key,
this.highlightPosition, this.highlightPosition,
this.highlightLabel, this.highlightLabel,
this.highlightMarkerKey,
this.highlightZoom = 15.0, this.highlightZoom = 15.0,
this.hideBackButton = false, this.hideBackButton = false,
}); });
@ -94,6 +97,19 @@ class _MapScreenState extends State<MapScreen> {
_removedMarkerIds = ids; _removedMarkerIds = ids;
_removedMarkersLoaded = true; _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<String>.from(_removedMarkerIds);
updated.remove(widget.highlightMarkerKey);
if (!mounted) return;
setState(() {
_removedMarkerIds = updated;
});
await _markerService.saveRemovedIds(updated);
}
} }
bool _checkLocationPlausibility(double lat, double lon) { bool _checkLocationPlausibility(double lat, double lon) {
@ -229,6 +245,24 @@ class _MapScreenState extends State<MapScreen> {
: <Polyline>[], : <Polyline>[],
); );
// Collect polylines for shared markers' history with dashed lines
final List<Polyline> sharedMarkerPolylines = [];
for (final marker in sharedMarkers) {
if (marker.history.isNotEmpty) {
final points = List<LatLng>.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) // Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0); LatLng center = const LatLng(0, 0);
double initialZoom = 10.0; double initialZoom = 10.0;
@ -475,6 +509,8 @@ class _MapScreenState extends State<MapScreen> {
), ),
if (_polylines.isNotEmpty && _isBuildingPathTrace) if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines), PolylineLayer(polylines: _polylines),
if (sharedMarkerPolylines.isNotEmpty)
PolylineLayer(polylines: sharedMarkerPolylines),
MarkerLayer( MarkerLayer(
markers: [ markers: [
if (highlightPosition != null) if (highlightPosition != null)
@ -1239,28 +1275,39 @@ class _MapScreenState extends State<MapScreen> {
} }
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) { 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 = <String, List<_SharedMarker>>{};
final selfName = connector.selfName ?? 'Me'; final selfName = connector.selfName ?? 'Me';
void addUpdate(_SharedMarker update) {
(updatesByKey[update.id] ??= <_SharedMarker>[]).add(update);
}
for (final contact in connector.contacts) { for (final contact in connector.contacts) {
final messages = connector.getMessages(contact); final messages = connector.getMessages(contact);
for (final message in messages) { for (final message in messages) {
final payload = _parseMarkerText(message.text); final payload = parseMarkerText(message.text);
if (payload == null) continue; if (payload == null) continue;
final fromName = message.isOutgoing ? selfName : contact.name; final fromName = message.isOutgoing ? selfName : contact.name;
final id = _buildMarkerId( final key = buildSharedMarkerKey(
sourceId: contact.publicKeyHex, sourceId: contact.publicKeyHex,
timestamp: message.timestamp, label: payload.label,
text: message.text, fromName: fromName,
flags: payload.flags,
isChannel: false,
); );
markers.add( addUpdate(
_SharedMarker( _SharedMarker(
id: id, id: key,
position: payload.position, position: payload.position,
label: payload.label, label: payload.label.isEmpty
? context.l10n.map_sharedPin
: payload.label,
flags: payload.flags, flags: payload.flags,
fromName: fromName, fromName: fromName,
sourceLabel: contact.name, sourceLabel: contact.name,
timestamp: message.timestamp,
isChannel: false, isChannel: false,
isPublicChannel: false, isPublicChannel: false,
), ),
@ -1272,23 +1319,28 @@ class _MapScreenState extends State<MapScreen> {
final isPublic = _isPublicChannel(channel); final isPublic = _isPublicChannel(channel);
final messages = connector.getChannelMessages(channel); final messages = connector.getChannelMessages(channel);
for (final message in messages) { for (final message in messages) {
final payload = _parseMarkerText(message.text); final payload = parseMarkerText(message.text);
if (payload == null) continue; if (payload == null) continue;
final id = _buildMarkerId( final key = buildSharedMarkerKey(
sourceId: 'channel:${channel.index}', sourceId: 'channel:${channel.index}',
timestamp: message.timestamp, label: payload.label,
text: message.text, fromName: message.senderName,
flags: payload.flags,
isChannel: true,
); );
markers.add( addUpdate(
_SharedMarker( _SharedMarker(
id: id, id: key,
position: payload.position, position: payload.position,
label: payload.label, label: payload.label.isEmpty
? context.l10n.map_sharedPin
: payload.label,
flags: payload.flags, flags: payload.flags,
fromName: message.senderName, fromName: message.senderName,
sourceLabel: channel.name.isEmpty sourceLabel: channel.name.isEmpty
? 'Channel ${channel.index}' ? 'Channel ${channel.index}'
: channel.name, : channel.name,
timestamp: message.timestamp,
isChannel: true, isChannel: true,
isPublicChannel: isPublic, isPublicChannel: isPublic,
), ),
@ -1296,36 +1348,25 @@ class _MapScreenState extends State<MapScreen> {
} }
} }
return markers; final markers = <_SharedMarker>[];
} updatesByKey.forEach((_, updates) {
updates.sort((a, b) => a.timestamp.compareTo(b.timestamp));
_MarkerPayload? _parseMarkerText(String text) { final latest = updates.last;
final trimmed = text.trim(); // History: older positions, drop consecutive duplicates at same position.
if (!trimmed.startsWith('m:')) return null; final history = <LatLng>[];
for (var i = 0; i < updates.length - 1; i++) {
final parts = trimmed.substring(2).split('|'); final p = updates[i].position;
if (parts.isEmpty) return null; if (history.isEmpty ||
final coords = parts[0].split(','); history.last.latitude != p.latitude ||
if (coords.length != 2) return null; history.last.longitude != p.longitude) {
final lat = double.tryParse(coords[0].trim()); history.add(p);
final lon = double.tryParse(coords[1].trim()); }
if (lat == null || lon == null) return null; }
markers.add(latest.copyWithHistory(history));
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({ markers.sort((a, b) => b.timestamp.compareTo(a.timestamp));
required String sourceId, return markers;
required DateTime timestamp,
required String text,
}) {
return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text';
} }
Marker _buildSharedMarker(_SharedMarker marker) { Marker _buildSharedMarker(_SharedMarker marker) {
@ -1337,7 +1378,15 @@ class _MapScreenState extends State<MapScreen> {
width: 60, width: 60,
height: 60, height: 60,
child: GestureDetector( 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( child: Column(
children: [ children: [
Container( Container(
@ -1391,11 +1440,17 @@ class _MapScreenState extends State<MapScreen> {
room: room, room: room,
// onLogin(password, isAdmin) isAdmin not used for room caht screen // onLogin(password, isAdmin) isAdmin not used for room caht screen
onLogin: (password, _) { onLogin: (password, _) {
// Navigate to chat screen after successful login final connector = context.read<MeshCoreConnector>();
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); final unread = connector.getUnreadCountForContactKey(
room.publicKeyHex,
);
connector.markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)), MaterialPageRoute(
builder: (context) =>
ChatScreen(contact: room, initialUnreadCount: unread),
),
); );
}, },
), ),
@ -1425,23 +1480,23 @@ class _MapScreenState extends State<MapScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildInfoRow('Type', contact.typeLabel), _buildInfoRow(context.l10n.map_type, contact.typeLabel(context.l10n)),
_buildInfoRow('Path', contact.pathLabel), _buildInfoRow(context.l10n.map_path, contact.pathLabel(context.l10n)),
if (contact.hasLocation) if (contact.hasLocation)
_buildInfoRow( _buildInfoRow(
'Location', context.l10n.map_location,
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
) )
else if (guessedPosition != null) else if (guessedPosition != null)
_buildInfoRow( _buildInfoRow(
'Est. Location', context.l10n.map_estLocation,
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
), ),
_buildInfoRow( _buildInfoRow(
context.l10n.map_lastSeen, context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen), _formatLastSeen(contact.lastSeen),
), ),
_buildInfoRow('Public Key', contact.publicKeyHex), _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
], ],
), ),
actions: [ actions: [
@ -1456,11 +1511,17 @@ class _MapScreenState extends State<MapScreen> {
if (!contact.isActive) { if (!contact.isActive) {
connector.importDiscoveredContact(contact); connector.importDiscoveredContact(contact);
} }
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChatScreen(contact: contact), builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
), ),
); );
}, },
@ -1542,7 +1603,9 @@ class _MapScreenState extends State<MapScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(marker.label), title: Text(
marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label,
),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1550,7 +1613,11 @@ class _MapScreenState extends State<MapScreen> {
_buildInfoRow(context.l10n.map_from, marker.fromName), _buildInfoRow(context.l10n.map_from, marker.fromName),
_buildInfoRow(context.l10n.map_source, marker.sourceLabel), _buildInfoRow(context.l10n.map_source, marker.sourceLabel),
_buildInfoRow( _buildInfoRow(
'Location', context.l10n.map_sharedAt,
_formatLastSeen(marker.timestamp),
),
_buildInfoRow(
context.l10n.map_location,
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}', '${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
), ),
if (marker.flags.isNotEmpty) if (marker.flags.isNotEmpty)
@ -1715,6 +1782,10 @@ class _MapScreenState extends State<MapScreen> {
String defaultLabel, String defaultLabel,
) async { ) async {
final controller = TextEditingController(text: defaultLabel); final controller = TextEditingController(text: defaultLabel);
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
);
return showDialog<String>( return showDialog<String>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
@ -2019,7 +2090,7 @@ class _MapScreenState extends State<MapScreen> {
enabled: settings.mapKeyPrefixEnabled, enabled: settings.mapKeyPrefixEnabled,
decoration: InputDecoration( decoration: InputDecoration(
labelText: context.l10n.map_publicKeyPrefix, labelText: context.l10n.map_publicKeyPrefix,
hintText: 'e.g. ab12', hintText: context.l10n.map_publicKeyPrefixHint,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
), ),
@ -2310,18 +2381,50 @@ class _GuessedLocation {
}); });
} }
class _MarkerPayload { class MarkerPayload {
final LatLng position; final LatLng position;
final String label; final String label;
final String flags; final String flags;
_MarkerPayload({ MarkerPayload({
required this.position, required this.position,
required this.label, required this.label,
required this.flags, required this.flags,
}); });
} }
/// Parse a shared marker text message of the form
/// `m:<lat>,<lon>|<label>|<flags>` and return a [MarkerPayload].
MarkerPayload? parseMarkerText(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) ?? '').trim();
final flags = (match.group(4) ?? '').trim();
return MarkerPayload(position: LatLng(lat, lon), label: label, flags: flags);
}
/// Build a normalized dedupe key for shared markers.
/// Keeps the same algorithm previously present in both chat and map screens.
String buildSharedMarkerKey({
required String sourceId,
required String label,
required String fromName,
required String flags,
required bool isChannel,
}) {
final normalizedLabel = label.trim().toLowerCase();
final normalizedFrom = fromName.trim().toLowerCase();
final normalizedFlags = flags.trim().toLowerCase();
final scope = isChannel ? 'ch' : 'dm';
return '$scope|$sourceId|$normalizedFrom|$normalizedLabel|$normalizedFlags';
}
class _SharedMarker { class _SharedMarker {
final String id; final String id;
final LatLng position; final LatLng position;
@ -2329,8 +2432,10 @@ class _SharedMarker {
final String flags; final String flags;
final String fromName; final String fromName;
final String sourceLabel; final String sourceLabel;
final DateTime timestamp;
final bool isChannel; final bool isChannel;
final bool isPublicChannel; final bool isPublicChannel;
final List<LatLng> history;
_SharedMarker({ _SharedMarker({
required this.id, required this.id,
@ -2339,7 +2444,24 @@ class _SharedMarker {
required this.flags, required this.flags,
required this.fromName, required this.fromName,
required this.sourceLabel, required this.sourceLabel,
required this.timestamp,
required this.isChannel, required this.isChannel,
required this.isPublicChannel, required this.isPublicChannel,
this.history = const [],
}); });
_SharedMarker copyWithHistory(List<LatLng> newHistory) {
return _SharedMarker(
id: id,
position: position,
label: label,
flags: flags,
fromName: fromName,
sourceLabel: sourceLabel,
timestamp: timestamp,
isChannel: isChannel,
isPublicChannel: isPublicChannel,
history: newHistory,
);
}
} }

@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import 'repeater_status_screen.dart'; import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart'; import 'repeater_cli_screen.dart';
@ -93,7 +94,7 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
repeater.pathLabel, repeater.pathLabel(context.l10n),
style: TextStyle(fontSize: 14, color: Colors.grey[600]), style: TextStyle(fontSize: 14, color: Colors.grey[600]),
), ),
if (repeater.hasLocation) ...[ if (repeater.hasLocation) ...[

@ -341,9 +341,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red), leading: const Icon(Icons.delete_outline, color: Colors.red),
title: Text("Delete All Paths"), title: Text(l10n.settings_deleteAllPaths),
subtitle: Text( subtitle: Text(
"Clear all path data from contacts.", l10n.settings_deleteAllPathsSubtitle,
style: TextStyle(color: Colors.red[700]), style: TextStyle(color: Colors.red[700]),
), ),
onTap: () => connector.deleteAllPaths(), onTap: () => connector.deleteAllPaths(),

@ -1,8 +1,19 @@
import '../utils/platform_info.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import '../l10n/app_localizations.dart';
import '../utils/platform_info.dart';
class BackgroundService { class BackgroundService {
bool _initialized = false; bool _initialized = false;
String? Function()? _languageOverrideProvider;
/// Allows the app to expose its current language override (e.g. from
/// AppSettingsService) so the foreground notification matches the app UI
/// language instead of only the system locale.
void setLanguageOverrideProvider(String? Function()? provider) {
_languageOverrideProvider = provider;
}
Future<void> initialize() async { Future<void> initialize() async {
if (!PlatformInfo.isAndroid || _initialized) return; if (!PlatformInfo.isAndroid || _initialized) return;
@ -34,13 +45,32 @@ class BackgroundService {
} }
final running = await FlutterForegroundTask.isRunningService; final running = await FlutterForegroundTask.isRunningService;
if (running) return; if (running) return;
final l10n = await _loadLocalizations();
await FlutterForegroundTask.startService( await FlutterForegroundTask.startService(
notificationTitle: 'MeshCore running', notificationTitle: l10n.background_serviceTitle,
notificationText: 'Keeping BLE connected', notificationText: l10n.background_serviceText,
callback: startCallback, callback: startCallback,
); );
} }
Future<AppLocalizations> _loadLocalizations() async {
final supported = AppLocalizations.supportedLocales;
final override = _languageOverrideProvider?.call();
if (override != null && override.isNotEmpty) {
final overrideLocale = Locale(override);
final isSupported = supported.any(
(l) => l.languageCode == overrideLocale.languageCode,
);
if (isSupported) {
return AppLocalizations.delegate.load(overrideLocale);
}
}
final preferred =
WidgetsBinding.instance.platformDispatcher.locales;
final match = basicLocaleListResolution(preferred, supported);
return AppLocalizations.delegate.load(match);
}
Future<void> stop() async { Future<void> stop() async {
if (!PlatformInfo.isAndroid) return; if (!PlatformInfo.isAndroid) return;
final running = await FlutterForegroundTask.isRunningService; final running = await FlutterForegroundTask.isRunningService;

@ -24,6 +24,26 @@ class LineOfSightSample {
}); });
} }
class LineOfSightObstruction {
final int sampleIndex;
final LatLng point;
final double distanceMeters;
final double clearanceMeters;
final double obstructionMeters;
final double terrainMeters;
final double lineHeightMeters;
const LineOfSightObstruction({
required this.sampleIndex,
required this.point,
required this.distanceMeters,
required this.clearanceMeters,
required this.obstructionMeters,
required this.terrainMeters,
required this.lineHeightMeters,
});
}
class LineOfSightResult { class LineOfSightResult {
final bool hasData; final bool hasData;
final bool isClear; final bool isClear;
@ -31,6 +51,7 @@ class LineOfSightResult {
final double maxObstructionMeters; final double maxObstructionMeters;
final double? firstObstructionDistanceMeters; final double? firstObstructionDistanceMeters;
final List<LineOfSightSample> samples; final List<LineOfSightSample> samples;
final List<LineOfSightObstruction> obstructions;
final String? errorMessage; final String? errorMessage;
final double usedKFactor; final double usedKFactor;
final double? frequencyMHz; final double? frequencyMHz;
@ -42,6 +63,7 @@ class LineOfSightResult {
required this.maxObstructionMeters, required this.maxObstructionMeters,
required this.firstObstructionDistanceMeters, required this.firstObstructionDistanceMeters,
required this.samples, required this.samples,
required this.obstructions,
required this.usedKFactor, required this.usedKFactor,
this.frequencyMHz, this.frequencyMHz,
this.errorMessage, this.errorMessage,
@ -56,7 +78,8 @@ class LineOfSightResult {
isClear = false, isClear = false,
maxObstructionMeters = 0, maxObstructionMeters = 0,
firstObstructionDistanceMeters = null, firstObstructionDistanceMeters = null,
samples = const []; samples = const [],
obstructions = const [];
} }
class LineOfSightPathSegment { class LineOfSightPathSegment {
@ -191,6 +214,7 @@ class LineOfSightService {
maxObstructionMeters: 0, maxObstructionMeters: 0,
firstObstructionDistanceMeters: null, firstObstructionDistanceMeters: null,
samples: const [], samples: const [],
obstructions: const [],
usedKFactor: kFactor, usedKFactor: kFactor,
frequencyMHz: frequencyMHz, frequencyMHz: frequencyMHz,
); );
@ -249,7 +273,9 @@ class LineOfSightService {
var maxObstructionMeters = 0.0; var maxObstructionMeters = 0.0;
double? firstObstructionDistanceMeters; double? firstObstructionDistanceMeters;
final samples = <LineOfSightSample>[]; final samples = <LineOfSightSample>[];
final obstructions = <LineOfSightObstruction>[];
var isClear = true; var isClear = true;
LineOfSightObstruction? clusterWorstObstruction;
for (int i = 0; i < points.length; i++) { for (int i = 0; i < points.length; i++) {
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1); final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
@ -274,6 +300,23 @@ class LineOfSightService {
maxObstructionMeters = obstruction; maxObstructionMeters = obstruction;
} }
firstObstructionDistanceMeters ??= distanceFromStart; firstObstructionDistanceMeters ??= distanceFromStart;
final candidate = LineOfSightObstruction(
sampleIndex: i,
point: points[i],
distanceMeters: distanceFromStart,
clearanceMeters: clearance,
obstructionMeters: obstruction,
terrainMeters: terrainHeight,
lineHeightMeters: lineHeight,
);
if (clusterWorstObstruction == null ||
candidate.obstructionMeters >
clusterWorstObstruction.obstructionMeters) {
clusterWorstObstruction = candidate;
}
} else if (clusterWorstObstruction != null) {
obstructions.add(clusterWorstObstruction);
clusterWorstObstruction = null;
} }
samples.add( samples.add(
@ -286,6 +329,9 @@ class LineOfSightService {
), ),
); );
} }
if (clusterWorstObstruction != null) {
obstructions.add(clusterWorstObstruction);
}
return LineOfSightResult( return LineOfSightResult(
hasData: true, hasData: true,
@ -294,6 +340,7 @@ class LineOfSightService {
maxObstructionMeters: maxObstructionMeters, maxObstructionMeters: maxObstructionMeters,
firstObstructionDistanceMeters: firstObstructionDistanceMeters, firstObstructionDistanceMeters: firstObstructionDistanceMeters,
samples: samples, samples: samples,
obstructions: obstructions,
usedKFactor: kFactor, usedKFactor: kFactor,
frequencyMHz: frequencyMHz, frequencyMHz: frequencyMHz,
); );

@ -0,0 +1,447 @@
import 'package:flutter/material.dart';
/// MeshCore redesign palette warm field-journal dark theme with
/// phosphor-green signal accents. Mirrors values from the redesign spec.
class MeshPalette {
MeshPalette._();
// Surfaces (warm near-black, olive undertone)
static const bg = Color(0xFF0F1412);
static const bg1 = Color(0xFF161C19);
static const bg2 = Color(0xFF1D2521);
static const bg3 = Color(0xFF28322D);
static const bg4 = Color(0xFF34403A);
// Lines
static const line = Color(0xFF232C28);
static const line2 = Color(0xFF34403A);
static const line3 = Color(0xFF48564F);
// Ink
static const ink = Color(0xFFEFF3E8);
static const ink2 = Color(0xFFBAC4B5);
static const ink3 = Color(0xFF7C8B82);
static const ink4 = Color(0xFF55635B);
// Signal (phosphor)
static const signal = Color(0xFF7BEFA8);
static const signalDim = Color(0xFF4DC580);
static const signalBg = Color(0x177BEFA8); // ~9% alpha
static const signalLine = Color(0x427BEFA8); // ~26%
static const signalGlow = Color(0x597BEFA8); // ~35%
// Warn (ember)
static const warn = Color(0xFFFFA552);
static const warnDim = Color(0xFFC27E3C);
static const warnBg = Color(0x1CFFA552);
static const warnLine = Color(0x4DFFA552);
// Alert (coral)
static const alert = Color(0xFFFF6A5C);
static const alertBg = Color(0x1CFF6A5C);
static const alertLine = Color(0x52FF6A5C);
// Blue (dusk sky)
static const blue = Color(0xFF7FCBF5);
static const blueBg = Color(0x1C7FCBF5);
static const blueLine = Color(0x477FCBF5);
// Magenta
static const magenta = Color(0xFFDE7FDB);
static const magentaBg = Color(0x1CDE7FDB);
static const magentaLine = Color(0x47DE7FDB);
// Me bubble (mossy)
static const me = Color(0xFF1E3527);
static const meBorder = Color(0xFF2D5039);
static const meInk = Color(0xFFDEF0DC);
// Light variant (used when user explicitly picks light theme)
static const lightBg = Color(0xFFF5F3EC);
static const lightBg1 = Color(0xFFECE9DF);
static const lightBg2 = Color(0xFFE2DED2);
static const lightLine = Color(0xFFCAC5B4);
static const lightInk = Color(0xFF0F1410);
static const lightInk2 = Color(0xFF3D463E);
static const lightInk3 = Color(0xFF6A756D);
static const lightSignal = Color(0xFF1A7A44);
}
/// Named font stacks Flutter falls back to system fonts when the named
/// family isn't installed, keeping things working without bundled assets.
class MeshFonts {
MeshFonts._();
static const sans = 'Inter';
static const mono = 'JetBrains Mono';
static const display = 'Instrument Serif';
static const List<String> sansFallback = [
'system-ui',
'-apple-system',
'Roboto',
'Noto Sans',
'sans-serif',
];
static const List<String> monoFallback = [
'SF Mono',
'Menlo',
'Consolas',
'Roboto Mono',
'monospace',
];
static const List<String> displayFallback = [
'Cormorant Garamond',
'Georgia',
'Times New Roman',
'serif',
];
}
/// Radii used consistently across the app.
class MeshRadii {
MeshRadii._();
static const xs = 6.0;
static const sm = 10.0;
static const md = 14.0;
static const lg = 18.0;
static const xl = 24.0;
static const pill = 999.0;
}
/// Shared helpers exposed via [MeshTheme.of].
class MeshTheme {
MeshTheme._();
static ThemeData dark() {
const scheme = ColorScheme.dark(
primary: MeshPalette.signal,
onPrimary: Color(0xFF0A1810),
primaryContainer: MeshPalette.signalBg,
onPrimaryContainer: MeshPalette.signal,
secondary: MeshPalette.blue,
onSecondary: Color(0xFF0A1520),
tertiary: MeshPalette.magenta,
onTertiary: Color(0xFF201020),
error: MeshPalette.alert,
onError: Color(0xFF1A0A08),
errorContainer: MeshPalette.alertBg,
onErrorContainer: MeshPalette.alert,
surface: MeshPalette.bg,
onSurface: MeshPalette.ink,
surfaceContainerLowest: MeshPalette.bg,
surfaceContainerLow: MeshPalette.bg1,
surfaceContainer: MeshPalette.bg1,
surfaceContainerHigh: MeshPalette.bg2,
surfaceContainerHighest: MeshPalette.bg3,
onSurfaceVariant: MeshPalette.ink2,
outline: MeshPalette.line2,
outlineVariant: MeshPalette.line,
shadow: Colors.black,
scrim: Colors.black54,
inverseSurface: MeshPalette.ink,
onInverseSurface: MeshPalette.bg,
inversePrimary: MeshPalette.signalDim,
);
return _build(scheme, Brightness.dark);
}
static ThemeData light() {
const scheme = ColorScheme.light(
primary: MeshPalette.lightSignal,
onPrimary: Colors.white,
primaryContainer: Color(0xFFD4E8D8),
onPrimaryContainer: MeshPalette.lightSignal,
secondary: Color(0xFF2F6EA8),
onSecondary: Colors.white,
tertiary: Color(0xFF8C4A8A),
onTertiary: Colors.white,
error: Color(0xFFB53D2F),
onError: Colors.white,
surface: MeshPalette.lightBg,
onSurface: MeshPalette.lightInk,
surfaceContainerLowest: MeshPalette.lightBg,
surfaceContainerLow: MeshPalette.lightBg1,
surfaceContainer: MeshPalette.lightBg1,
surfaceContainerHigh: MeshPalette.lightBg2,
surfaceContainerHighest: Color(0xFFD5D0C0),
onSurfaceVariant: MeshPalette.lightInk2,
outline: MeshPalette.lightLine,
outlineVariant: Color(0xFFDBD6C6),
);
return _build(scheme, Brightness.light);
}
static ThemeData _build(ColorScheme scheme, Brightness brightness) {
final baseText =
Typography.material2021(
platform: TargetPlatform.android,
colorScheme: scheme,
).black.apply(
bodyColor: scheme.onSurface,
displayColor: scheme.onSurface,
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
canvasColor: scheme.surface,
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
textTheme: baseText,
dividerColor: scheme.outlineVariant,
dividerTheme: DividerThemeData(
color: scheme.outlineVariant,
thickness: 1,
space: 1,
),
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
foregroundColor: scheme.onSurface,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
fontSize: 20,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
color: scheme.onSurface,
),
iconTheme: IconThemeData(color: scheme.onSurface),
shape: Border(
bottom: BorderSide(color: scheme.outlineVariant, width: 1),
),
),
cardTheme: CardThemeData(
color: scheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
side: BorderSide(color: scheme.outlineVariant, width: 1),
),
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
),
listTileTheme: ListTileThemeData(
iconColor: scheme.onSurfaceVariant,
textColor: scheme.onSurface,
tileColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
elevation: 0,
focusElevation: 0,
hoverElevation: 0,
highlightElevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
),
extendedTextStyle: const TextStyle(
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
),
textStyle: const TextStyle(
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: scheme.onSurface,
side: BorderSide(color: scheme.outline),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: scheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: scheme.surfaceContainerHigh,
hintStyle: TextStyle(color: scheme.onSurfaceVariant),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide(color: scheme.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide(color: scheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide(color: scheme.primary, width: 1.5),
),
),
chipTheme: ChipThemeData(
backgroundColor: scheme.surfaceContainerLow,
side: BorderSide(color: scheme.outlineVariant),
labelStyle: TextStyle(
fontFamily: MeshFonts.sans,
fontFamilyFallback: MeshFonts.sansFallback,
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: scheme.onSurfaceVariant,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: scheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
indicatorColor: scheme.primary.withValues(alpha: 0.14),
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return TextStyle(
fontFamily: MeshFonts.mono,
fontFamilyFallback: MeshFonts.monoFallback,
fontSize: 10,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
letterSpacing: 0.1,
color: selected ? scheme.primary : scheme.onSurfaceVariant,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return IconThemeData(
color: selected ? scheme.primary : scheme.onSurfaceVariant,
size: 22,
);
}),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: scheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
modalBackgroundColor: scheme.surfaceContainerLow,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(MeshRadii.lg),
),
),
),
dialogTheme: DialogThemeData(
backgroundColor: scheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.lg),
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: scheme.surfaceContainerHigh,
contentTextStyle: TextStyle(color: scheme.onSurface),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
),
),
popupMenuTheme: PopupMenuThemeData(
color: scheme.surfaceContainerHigh,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.md),
),
),
iconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22),
splashFactory: InkSparkle.splashFactory,
);
}
/// Mono text style sizes default to the body size Inter is using.
static TextStyle mono({
double? fontSize,
FontWeight? fontWeight,
Color? color,
double? letterSpacing,
}) {
return TextStyle(
fontFamily: MeshFonts.mono,
fontFamilyFallback: MeshFonts.monoFallback,
fontSize: fontSize,
fontWeight: fontWeight,
color: color,
letterSpacing: letterSpacing ?? 0.2,
fontFeatures: const [FontFeature.tabularFigures()],
);
}
/// Serif display style.
static TextStyle display({
double? fontSize,
FontWeight? fontWeight,
Color? color,
double? letterSpacing,
}) {
return TextStyle(
fontFamily: MeshFonts.display,
fontFamilyFallback: MeshFonts.displayFallback,
fontSize: fontSize,
fontWeight: fontWeight ?? FontWeight.w400,
color: color,
letterSpacing: letterSpacing ?? -0.2,
);
}
/// Small-caps mono label used for section accents and chip labels.
static TextStyle accentLabel({Color? color, double? fontSize}) {
return TextStyle(
fontFamily: MeshFonts.mono,
fontFamilyFallback: MeshFonts.monoFallback,
fontSize: fontSize ?? 9.5,
fontWeight: FontWeight.w600,
letterSpacing: 1.8,
color: color,
);
}
/// Color-code an SNR value for consistency across the app.
static Color snrColor(num? snr, {required bool blocked}) {
if (blocked) return MeshPalette.alert;
if (snr == null) return MeshPalette.ink3;
if (snr > -5) return MeshPalette.signal;
if (snr > -12) return MeshPalette.warn;
return MeshPalette.alert;
}
}

@ -72,8 +72,8 @@ class GpxExport {
contact.name, contact.name,
contact.latitude!, contact.latitude!,
contact.longitude!, contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url, url,
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
); );
} }
} }
@ -91,8 +91,8 @@ class GpxExport {
contact.name, contact.name,
contact.latitude!, contact.latitude!,
contact.longitude!, contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url, url,
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
); );
} }
} }
@ -110,8 +110,8 @@ class GpxExport {
contact.name, contact.name,
contact.latitude ?? 0.0, contact.latitude ?? 0.0,
contact.longitude ?? 0.0, contact.longitude ?? 0.0,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url, url,
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
); );
} }
} }

@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
@ -147,7 +148,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
context, context,
availableContacts: availableContacts, availableContacts: availableContacts,
initialPath: pathForInput.isEmpty ? null : pathForInput, initialPath: pathForInput.isEmpty ? null : pathForInput,
currentPathLabel: currentContact.pathLabel, currentPathLabel: currentContact.pathLabel(l10n),
onRefresh: connector.isConnected ? connector.getContacts : null, onRefresh: connector.isConnected ? connector.getContacts : null,
); );
@ -236,7 +237,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
l10n.path_currentPath(currentContact.pathLabel), l10n.path_currentPath(currentContact.pathLabel(l10n)),
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -303,7 +304,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
), ),
isThreeLine: true, isThreeLine: true,
subtitle: Text( subtitle: Text(
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes}Score: ${path.routeWeight.toStringAsFixed(1)}', '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes}${l10n.chat_score}: ${path.routeWeight.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 11), style: const TextStyle(fontSize: 11),
), ),
trailing: Row( trailing: Row(

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
class PathSelectionDialog extends StatefulWidget { class PathSelectionDialog extends StatefulWidget {
@ -311,7 +312,7 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
subtitle: Text( subtitle: Text(
'${contact.typeLabel}${contact.publicKeyHex.substring(0, 2)}', '${contact.typeLabel(l10n)}${contact.publicKeyHex.substring(0, 2)}',
style: const TextStyle(fontSize: 10), style: const TextStyle(fontSize: 10),
), ),
trailing: isSelected trailing: isSelected

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import '../l10n/l10n.dart';
/// A reusable QR code display widget for sharing data. /// A reusable QR code display widget for sharing data.
/// ///
/// Features: /// Features:
@ -197,7 +199,7 @@ class QrCodeShareDialog extends StatelessWidget {
width: double.infinity, width: double.infinity,
child: FilledButton( child: FilledButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Done'), child: Text(context.l10n.common_done),
), ),
), ),
], ],

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
@ -467,7 +468,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
repeater.pathLabel, repeater.pathLabel(context.l10n),
style: const TextStyle(fontSize: 11, color: Colors.grey), style: const TextStyle(fontSize: 11, color: Colors.grey),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../l10n/contact_localization.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
@ -393,7 +394,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
repeater.pathLabel, repeater.pathLabel(context.l10n),
style: const TextStyle(fontSize: 11, color: Colors.grey), style: const TextStyle(fontSize: 11, color: Colors.grey),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
class UnreadDivider extends StatelessWidget {
const UnreadDivider({super.key});
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.primary;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Expanded(child: Divider(color: color)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.chat_newMessages,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Expanded(child: Divider(color: color)),
],
),
);
}
}

@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

@ -0,0 +1,105 @@
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/l10n/contact_localization.dart';
import 'package:meshcore_open/models/contact.dart';
Contact _contact({
int type = advTypeChat,
int pathLength = 0,
int? pathOverride,
}) {
return Contact(
publicKey: Uint8List.fromList(List.generate(32, (i) => i + 1)),
name: 'Node',
type: type,
pathLength: pathLength,
path: Uint8List(0),
pathOverride: pathOverride,
lastSeen: DateTime.fromMillisecondsSinceEpoch(0),
);
}
void main() {
late AppLocalizations l10n;
setUpAll(() async {
l10n = await AppLocalizations.delegate.load(const Locale('en'));
});
group('Contact.typeLabel', () {
test('chat', () {
expect(_contact(type: advTypeChat).typeLabel(l10n), 'Chat');
});
test('repeater', () {
expect(
_contact(type: advTypeRepeater).typeLabel(l10n),
'Repeater',
);
});
test('room', () {
expect(_contact(type: advTypeRoom).typeLabel(l10n), 'Room');
});
test('sensor', () {
expect(_contact(type: advTypeSensor).typeLabel(l10n), 'Sensor');
});
test('unknown type falls back', () {
expect(_contact(type: 99).typeLabel(l10n), 'Unknown');
});
});
group('Contact.pathLabel (override)', () {
test('override < 0 -> flood (forced)', () {
expect(
_contact(pathOverride: -1).pathLabel(l10n),
'Flood (forced)',
);
});
test('override == 0 -> direct (forced)', () {
expect(
_contact(pathOverride: 0).pathLabel(l10n),
'Direct (forced)',
);
});
test('override > 0 -> hops (forced)', () {
expect(
_contact(pathOverride: 3).pathLabel(l10n),
'3 hops (forced)',
);
});
test('override takes precedence over pathLength', () {
expect(
_contact(pathLength: 5, pathOverride: -1).pathLabel(l10n),
'Flood (forced)',
);
});
});
group('Contact.pathLabel (auto)', () {
test('pathLength < 0 -> flood', () {
expect(_contact(pathLength: -1).pathLabel(l10n), 'Flood');
});
test('pathLength == 0 -> direct', () {
expect(_contact(pathLength: 0).pathLabel(l10n), 'Direct');
});
test('pathLength == 1 -> singular hop', () {
expect(_contact(pathLength: 1).pathLabel(l10n), '1 hop');
});
test('pathLength > 1 -> plural hops', () {
expect(_contact(pathLength: 4).pathLabel(l10n), '4 hops');
});
});
}

@ -23,6 +23,7 @@ void main() {
expect(result.isClear, isTrue); expect(result.isClear, isTrue);
expect(result.maxObstructionMeters, equals(0)); expect(result.maxObstructionMeters, equals(0));
expect(result.firstObstructionDistanceMeters, isNull); expect(result.firstObstructionDistanceMeters, isNull);
expect(result.obstructions, isEmpty);
}); });
test( test(
@ -44,9 +45,32 @@ void main() {
expect(result.isClear, isFalse); expect(result.isClear, isFalse);
expect(result.maxObstructionMeters, greaterThan(0)); expect(result.maxObstructionMeters, greaterThan(0));
expect(result.firstObstructionDistanceMeters, isNotNull); expect(result.firstObstructionDistanceMeters, isNotNull);
expect(result.obstructions, hasLength(1));
expect(result.obstructions.single.sampleIndex, equals(10));
expect(result.obstructions.single.point, equals(points[10]));
}, },
); );
test('computeFromElevations groups contiguous blocked samples', () {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
elevations[9] = 220;
elevations[10] = 320;
elevations[11] = 240;
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 1.5,
endAntennaHeightMeters: 1.5,
kFactor: 4.0 / 3.0,
);
expect(result.obstructions, hasLength(1));
expect(result.obstructions.single.sampleIndex, equals(10));
expect(result.obstructions.single.obstructionMeters, greaterThan(0));
});
test('analyzePath summarizes clear and blocked segments', () async { test('analyzePath summarizes clear and blocked segments', () async {
final service = LineOfSightService( final service = LineOfSightService(
elevationDataSource: (points) async { elevationDataSource: (points) async {

@ -1 +1,86 @@
{} {
"bg": [
"chat_markAsUnread",
"chat_newMessages"
],
"de": [
"chat_markAsUnread",
"chat_newMessages"
],
"es": [
"chat_markAsUnread",
"chat_newMessages"
],
"fr": [
"chat_markAsUnread",
"chat_newMessages"
],
"hu": [
"chat_markAsUnread",
"chat_newMessages"
],
"it": [
"chat_markAsUnread",
"chat_newMessages"
],
"ja": [
"chat_markAsUnread",
"chat_newMessages"
],
"ko": [
"chat_markAsUnread",
"chat_newMessages"
],
"nl": [
"chat_markAsUnread",
"chat_newMessages"
],
"pl": [
"chat_markAsUnread",
"chat_newMessages"
],
"pt": [
"chat_markAsUnread",
"chat_newMessages"
],
"ru": [
"chat_markAsUnread",
"chat_newMessages"
],
"sk": [
"chat_markAsUnread",
"chat_newMessages"
],
"sl": [
"chat_markAsUnread",
"chat_newMessages"
],
"sv": [
"chat_markAsUnread",
"chat_newMessages"
],
"uk": [
"chat_markAsUnread",
"chat_newMessages"
],
"zh": [
"chat_markAsUnread",
"chat_newMessages"
]
}

@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
flutter_local_notifications_windows flutter_local_notifications_windows
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

Loading…
Cancel
Save

Powered by TurnKey Linux.