tailscale: translate the peer "last seen" time (#3922)

(cherry picked from commit 0691242c9a)
This commit is contained in:
Marlon Kranz authored and bbedward committed 2026-10-26 13:37:26 -04:00
1 parent 5a210112aa
commit 2d7b65a8c3
3 files changed
+13 -50

No files matched your search

+1 -29
View File
@@ -1,7 +1,6 @@
package tailscale
import (
"fmt"
"sort"
"strings"
"time"
@@ -103,35 +102,8 @@ func convertPeerStatus(ps *ipnstate.PeerStatus, users map[tailcfg.UserID]tailcfg
}
if !ps.LastSeen.IsZero() {
peer.LastSeen = formatRelativeTime(ps.LastSeen)
peer.LastSeen = ps.LastSeen.UTC().Format(time.RFC3339)
}
return peer
}
// formatRelativeTime formats a time as a human-readable relative duration (e.g., "5 minutes ago").
func formatRelativeTime(t time.Time) string {
d := time.Since(t)
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
m := int(d.Minutes())
if m == 1 {
return "1 minute ago"
}
return fmt.Sprintf("%d minutes ago", m)
case d < 24*time.Hour:
h := int(d.Hours())
if h == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", h)
default:
days := int(d.Hours() / 24)
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
}
}
@@ -169,23 +169,3 @@ func TestPeerSorting(t *testing.T) {
assert.Equal(t, "beta", state.Peers[1].Hostname)
assert.Equal(t, "zebra", state.Peers[2].Hostname)
}
func TestFormatRelativeTime(t *testing.T) {
tests := []struct {
name string
duration string
contains string
}{
{"minutes", "5m", "minutes ago"},
{"hours", "3h", "hours ago"},
{"days", "48h", "days ago"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d, _ := time.ParseDuration(tt.duration)
result := formatRelativeTime(time.Now().Add(-d))
assert.Contains(t, result, tt.contains)
})
}
}
@@ -48,6 +48,17 @@ PluginComponent {
property int filterIndex: 0
property string expandedHostname: ""
function formatLastSeen(iso) {
const date = new Date(iso);
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
if (minutes < 60)
return I18n.tr("%1m ago", "Tailscale peer last seen time, %1 is a number of minutes").arg(Math.max(1, minutes));
const hours = Math.floor(minutes / 60);
if (hours < 24)
return I18n.tr("%1h ago", "Tailscale peer last seen time, %1 is a number of hours").arg(hours);
return I18n.tr("%1d ago", "Tailscale peer last seen time, %1 is a number of days").arg(Math.floor(hours / 24));
}
readonly property string title: I18n.tr("Tailscale", "Tailscale mesh VPN widget title")
readonly property string noneLabel: I18n.tr("None", "Tailscale exit node: none selected")
readonly property var filteredPeers: {
@@ -207,7 +218,7 @@ PluginComponent {
if (modelData.online)
parts.push(modelData.relay ? I18n.tr("relay: %1", "Tailscale relay server name").arg(modelData.relay) : I18n.tr("direct", "Tailscale direct connection"));
else if (modelData.lastSeen)
parts.push(I18n.tr("last seen %1", "Tailscale peer last seen time").arg(modelData.lastSeen));
parts.push(I18n.tr("last seen %1", "Tailscale peer last seen time").arg(detailRoot.formatLastSeen(modelData.lastSeen)));
return parts.join(" • ");
}
trailingBadge: isSelf ? I18n.tr("This device", "Label for the user's own device in Tailscale") : ""