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 =