feat: Localize traceroute strings (#4228)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-15 10:43:55 -06:00 committed by GitHub
parent 1f07486745
commit 37b59af27b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 67 additions and 21 deletions

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model
import org.jetbrains.compose.resources.StringResource
@ -72,25 +71,36 @@ private fun formatTraceroutePath(nodesList: List<String>, snrList: List<Int>): 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,

View file

@ -442,6 +442,10 @@
<string name="view_on_map">View on map</string>
<string name="traceroute_map_no_data">This traceroute does not have any mappable nodes yet.</string>
<string name="traceroute_showing_nodes">Showing %1$d/%2$d nodes</string>
<string name="traceroute_duration">Duration: %1$s s</string>
<string name="traceroute_time_and_text">%1$s - %2$s</string>
<string name="traceroute_route_towards_dest">Route traced toward destination:\n\n</string>
<string name="traceroute_route_back_to_us">Route traced back to us:\n\n</string>
<string name="twenty_four_hours">24H</string>
<string name="forty_eight_hours">48H</string>
<string name="one_week">1W</string>

View file

@ -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 =