From 37b59af27b37ee889c179609d9c17a933790ca16 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:43:55 -0600 Subject: [PATCH] feat: Localize traceroute strings (#4228) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/service/MeshTracerouteHandler.kt | 18 ++++++--- .../meshtastic/core/model/RouteDiscovery.kt | 28 +++++++++----- .../composeResources/values/strings.xml | 4 ++ .../feature/node/metrics/TracerouteLog.kt | 38 +++++++++++++++---- 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt index b4cdc77a1..104e50ccb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt @@ -29,6 +29,9 @@ import org.meshtastic.core.model.getFullTracerouteResponse import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.core.strings.Res +import org.meshtastic.core.strings.traceroute_duration +import org.meshtastic.core.strings.traceroute_route_back_to_us +import org.meshtastic.core.strings.traceroute_route_towards_dest import org.meshtastic.core.strings.unknown_username import org.meshtastic.proto.MeshProtos.MeshPacket import java.util.Locale @@ -53,10 +56,14 @@ constructor( fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { val full = - packet.getFullTracerouteResponse { num -> - nodeManager.nodeDBbyNodeNum[num]?.let { "${it.longName} (${it.shortName})" } - ?: getString(Res.string.unknown_username) - } ?: return + packet.getFullTracerouteResponse( + getUser = { num -> + nodeManager.nodeDBbyNodeNum[num]?.let { "${it.longName} (${it.shortName})" } + ?: getString(Res.string.unknown_username) + }, + headerTowards = getString(Res.string.traceroute_route_towards_dest), + headerBack = getString(Res.string.traceroute_route_back_to_us), + ) ?: return val requestId = packet.decoded.requestId if (logUuid != null) { @@ -79,7 +86,8 @@ constructor( val elapsedMs = System.currentTimeMillis() - start val seconds = elapsedMs / MILLISECONDS_IN_SECOND Logger.i { "Traceroute $requestId complete in $seconds s" } - String.format(Locale.US, "%s\n\nDuration: %.1f s", full, seconds) + val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds)) + "$full\n\n$durationText" } else { full } diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt index e27d2d3d8..e60e85e0d 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.model import org.jetbrains.compose.resources.StringResource @@ -72,25 +71,36 @@ private fun formatTraceroutePath(nodesList: List, snrList: List): S .joinToString("\n") } -private fun RouteDiscovery.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String = buildString { +private fun RouteDiscovery.getTracerouteResponse( + getUser: (nodeNum: Int) -> String, + headerTowards: String = "Route traced toward destination:\n\n", + headerBack: String = "Route traced back to us:\n\n", +): String = buildString { if (routeList.isNotEmpty()) { - append("Route traced toward destination:\n\n") + append(headerTowards) append(formatTraceroutePath(routeList.map(getUser), snrTowardsList)) } if (routeBackList.isNotEmpty()) { append("\n\n") - append("Route traced back to us:\n\n") + append(headerBack) append(formatTraceroutePath(routeBackList.map(getUser), snrBackList)) } } -fun MeshProtos.MeshPacket.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = - fullRouteDiscovery?.getTracerouteResponse(getUser) +fun MeshProtos.MeshPacket.getTracerouteResponse( + getUser: (nodeNum: Int) -> String, + headerTowards: String = "Route traced toward destination:\n\n", + headerBack: String = "Route traced back to us:\n\n", +): String? = fullRouteDiscovery?.getTracerouteResponse(getUser, headerTowards, headerBack) /** Returns a traceroute response string only when the result is complete (both directions). */ -fun MeshProtos.MeshPacket.getFullTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = fullRouteDiscovery +fun MeshProtos.MeshPacket.getFullTracerouteResponse( + getUser: (nodeNum: Int) -> String, + headerTowards: String = "Route traced toward destination:\n\n", + headerBack: String = "Route traced back to us:\n\n", +): String? = fullRouteDiscovery ?.takeIf { it.routeList.isNotEmpty() && it.routeBackList.isNotEmpty() } - ?.getTracerouteResponse(getUser) + ?.getTracerouteResponse(getUser, headerTowards, headerBack) enum class TracerouteMapAvailability { Ok, diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index f1ee4ff0d..f329c0762 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -442,6 +442,10 @@ View on map This traceroute does not have any mappable nodes yet. Showing %1$d/%2$d nodes + Duration: %1$s s + %1$s - %2$s + Route traced toward destination:\n\n + Route traced back to us:\n\n 24H 48H 1W diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index edd92dab3..e73e4c943 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.meshtastic.core.strings.getString import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource @@ -76,7 +77,11 @@ import org.meshtastic.core.strings.routing_error_no_response import org.meshtastic.core.strings.traceroute import org.meshtastic.core.strings.traceroute_diff import org.meshtastic.core.strings.traceroute_direct +import org.meshtastic.core.strings.traceroute_duration import org.meshtastic.core.strings.traceroute_hops +import org.meshtastic.core.strings.traceroute_route_back_to_us +import org.meshtastic.core.strings.traceroute_route_towards_dest +import org.meshtastic.core.strings.traceroute_time_and_text import org.meshtastic.core.strings.view_on_map import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD @@ -165,14 +170,27 @@ fun TracerouteLogScreen( val seconds = (res.received_date - log.received_date).coerceAtLeast(0).toDouble() / MS_PER_SEC val annotatedBase = - annotateTraceroute(res.fromRadio.packet.getTracerouteResponse(::getUsername)) + annotateTraceroute( + res.fromRadio.packet.getTracerouteResponse( + ::getUsername, + headerTowards = stringResource(Res.string.traceroute_route_towards_dest), + headerBack = stringResource(Res.string.traceroute_route_back_to_us), + ), + ) + val durationText = stringResource(Res.string.traceroute_duration, "%.1f".format(seconds)) buildAnnotatedString { append(annotatedBase) - append("\n\nDuration: ${"%.1f".format(seconds)} s") + append("\n\n$durationText") } } else { // For cases where there's a result but no full route, display plain text - res.fromRadio.packet.getTracerouteResponse(::getUsername)?.let { AnnotatedString(it) } + res.fromRadio.packet + .getTracerouteResponse( + ::getUsername, + headerTowards = stringResource(Res.string.traceroute_route_towards_dest), + headerBack = stringResource(Res.string.traceroute_route_back_to_us), + ) + ?.let { AnnotatedString(it) } } } val overlay = @@ -187,14 +205,20 @@ fun TracerouteLogScreen( Box { TracerouteItem( icon = icon, - text = "$time - $text", + text = stringResource(Res.string.traceroute_time_and_text, time, text), modifier = Modifier.combinedClickable(onLongClick = { expanded = true }) { val dialogMessage = tracerouteDetailsAnnotated - ?: result?.fromRadio?.packet?.getTracerouteResponse(::getUsername)?.let { - AnnotatedString(it) - } + ?: result + ?.fromRadio + ?.packet + ?.getTracerouteResponse( + ::getUsername, + headerTowards = getString(Res.string.traceroute_route_towards_dest), + headerBack = getString(Res.string.traceroute_route_back_to_us), + ) + ?.let { AnnotatedString(it) } dialogMessage?.let { val responseLogUuid = result?.uuid ?: return@combinedClickable showDialog =