fix(i18n): relative-time strings vollständig lokalisiert
relativeTime() in Dashboard, relTime() in WireGuard Clients+Servers und
CertExpiry() im Cluster-Page haben bisher hardcodiertes Deutsch
(„vor 5s", „abgelaufen vor X Tagen") gerendert, unabhängig von der
gewählten UI-Sprache.
Neu: gemeinsame i18n-Keys common.relTime.{Xs,Xm,Xh,Xd,never} in EN+DE,
sowie cluster.{certDaysRemaining,certExpiredDaysAgo}. Alle Helper-
Funktionen nehmen jetzt t() als Parameter und sind damit sprachunabhängig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ import (
|
|||||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.75"
|
var version = "1.1.76"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -436,6 +436,8 @@
|
|||||||
"renewSelfConfirm": "Peer-Cert mit lokaler CA neu signieren (1 Jahr)? edgeguard-api Restart danach erforderlich.",
|
"renewSelfConfirm": "Peer-Cert mit lokaler CA neu signieren (1 Jahr)? edgeguard-api Restart danach erforderlich.",
|
||||||
"certRenewedRestartHint": "Cert erneuert — bitte sudo systemctl restart edgeguard-api ausführen.",
|
"certRenewedRestartHint": "Cert erneuert — bitte sudo systemctl restart edgeguard-api ausführen.",
|
||||||
"certRenewFailed": "Cert-Erneuerung fehlgeschlagen",
|
"certRenewFailed": "Cert-Erneuerung fehlgeschlagen",
|
||||||
|
"certDaysRemaining": "{{n}} Tage",
|
||||||
|
"certExpiredDaysAgo": "abgelaufen vor {{n}} Tagen",
|
||||||
"removePeerBtn": "Entfernen",
|
"removePeerBtn": "Entfernen",
|
||||||
"removePeerConfirmTitle": "Peer wirklich aus dem Cluster entfernen?",
|
"removePeerConfirmTitle": "Peer wirklich aus dem Cluster entfernen?",
|
||||||
"removePeerConfirmDesc": "{{fqdn}} wird aus ha_nodes gelöscht. Firewall-Renderer entfernt seine IP aus dem peer_ipv4-Set. Auf der Peer-Seite läuft edgeguard-api weiter; manuell stoppen + cluster-tls löschen für vollständigen Decommission.",
|
"removePeerConfirmDesc": "{{fqdn}} wird aus ha_nodes gelöscht. Firewall-Renderer entfernt seine IP aus dem peer_ipv4-Set. Auf der Peer-Seite läuft edgeguard-api weiter; manuell stoppen + cluster-tls löschen für vollständigen Decommission.",
|
||||||
@@ -910,7 +912,14 @@
|
|||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copied": "Kopiert",
|
"copied": "Kopiert",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"refresh": "Aktualisieren"
|
"refresh": "Aktualisieren",
|
||||||
|
"relTime": {
|
||||||
|
"Xs": "vor {{n}}s",
|
||||||
|
"Xm": "vor {{n}}m",
|
||||||
|
"Xh": "vor {{n}}h",
|
||||||
|
"Xd": "vor {{n}}d",
|
||||||
|
"never": "nie"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "Lizenz",
|
"title": "Lizenz",
|
||||||
|
|||||||
@@ -436,6 +436,8 @@
|
|||||||
"renewSelfConfirm": "Re-sign peer cert with the local CA (1 year)? edgeguard-api restart required after.",
|
"renewSelfConfirm": "Re-sign peer cert with the local CA (1 year)? edgeguard-api restart required after.",
|
||||||
"certRenewedRestartHint": "Cert renewed — please run sudo systemctl restart edgeguard-api.",
|
"certRenewedRestartHint": "Cert renewed — please run sudo systemctl restart edgeguard-api.",
|
||||||
"certRenewFailed": "Cert renewal failed",
|
"certRenewFailed": "Cert renewal failed",
|
||||||
|
"certDaysRemaining": "{{n}} days",
|
||||||
|
"certExpiredDaysAgo": "expired {{n}} d ago",
|
||||||
"removePeerBtn": "Remove",
|
"removePeerBtn": "Remove",
|
||||||
"removePeerConfirmTitle": "Really remove peer from the cluster?",
|
"removePeerConfirmTitle": "Really remove peer from the cluster?",
|
||||||
"removePeerConfirmDesc": "{{fqdn}} is deleted from ha_nodes. Firewall renderer drops its IP from the peer_ipv4 set. On the peer side edgeguard-api keeps running; manually stop it + delete cluster-tls for a full decommission.",
|
"removePeerConfirmDesc": "{{fqdn}} is deleted from ha_nodes. Firewall renderer drops its IP from the peer_ipv4 set. On the peer side edgeguard-api keeps running; manually stop it + delete cluster-tls for a full decommission.",
|
||||||
@@ -910,7 +912,14 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh",
|
||||||
|
"relTime": {
|
||||||
|
"Xs": "{{n}}s ago",
|
||||||
|
"Xm": "{{n}}m ago",
|
||||||
|
"Xh": "{{n}}h ago",
|
||||||
|
"Xd": "{{n}}d ago",
|
||||||
|
"never": "never"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "License",
|
"title": "License",
|
||||||
|
|||||||
@@ -541,12 +541,15 @@ export default function ClusterPage() {
|
|||||||
// CertExpiry rendert "<n> Tage" mit Farbcode: rot < 30, orange < 90,
|
// CertExpiry rendert "<n> Tage" mit Farbcode: rot < 30, orange < 90,
|
||||||
// grün sonst. Tooltip zeigt das absolute NotAfter-Datum.
|
// grün sonst. Tooltip zeigt das absolute NotAfter-Datum.
|
||||||
function CertExpiry({ days, until }: { days: number; until: string }) {
|
function CertExpiry({ days, until }: { days: number; until: string }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
let color: string | undefined
|
let color: string | undefined
|
||||||
if (days < 0) color = '#cf1322' // already expired
|
if (days < 0) color = '#cf1322' // already expired
|
||||||
else if (days < 30) color = '#cf1322' // critical
|
else if (days < 30) color = '#cf1322' // critical
|
||||||
else if (days < 90) color = '#d4651a' // warning
|
else if (days < 90) color = '#d4651a' // warning
|
||||||
else color = '#52c41a' // healthy
|
else color = '#52c41a' // healthy
|
||||||
const label = days < 0 ? `abgelaufen vor ${-days} Tagen` : `${days} Tage`
|
const label = days < 0
|
||||||
|
? t('cluster.certExpiredDaysAgo', { n: -days })
|
||||||
|
: t('cluster.certDaysRemaining', { n: days })
|
||||||
return (
|
return (
|
||||||
<Tooltip title={new Date(until).toLocaleString()}>
|
<Tooltip title={new Date(until).toLocaleString()}>
|
||||||
<Tag color={color === '#52c41a' ? 'green' : color === '#d4651a' ? 'orange' : 'red'}>
|
<Tag color={color === '#52c41a' ? 'green' : color === '#d4651a' ? 'orange' : 'red'}>
|
||||||
|
|||||||
@@ -131,13 +131,13 @@ function formatBytes(n: number): string {
|
|||||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
||||||
return `${v.toFixed(1)} ${u[i]}`
|
return `${v.toFixed(1)} ${u[i]}`
|
||||||
}
|
}
|
||||||
function relativeTime(unix: number): string {
|
function relativeTime(unix: number, t: (k: string, v?: Record<string, unknown>) => string): string {
|
||||||
if (!unix) return 'nie'
|
if (!unix) return t('common.relTime.never')
|
||||||
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
||||||
if (sec < 60) return `vor ${sec}s`
|
if (sec < 60) return t('common.relTime.Xs', { n: sec })
|
||||||
if (sec < 3600) return `vor ${Math.floor(sec / 60)}m`
|
if (sec < 3600) return t('common.relTime.Xm', { n: Math.floor(sec / 60) })
|
||||||
if (sec < 86400) return `vor ${Math.floor(sec / 3600)}h`
|
if (sec < 86400) return t('common.relTime.Xh', { n: Math.floor(sec / 3600) })
|
||||||
return `vor ${Math.floor(sec / 86400)}d`
|
return t('common.relTime.Xd', { n: Math.floor(sec / 86400) })
|
||||||
}
|
}
|
||||||
function formatUptime(sec: number): string {
|
function formatUptime(sec: number): string {
|
||||||
if (sec < 60) return `${sec}s`
|
if (sec < 60) return `${sec}s`
|
||||||
@@ -148,10 +148,10 @@ function formatUptime(sec: number): string {
|
|||||||
if (h > 0) return `${h}h ${m}m`
|
if (h > 0) return `${h}h ${m}m`
|
||||||
return `${m}m`
|
return `${m}m`
|
||||||
}
|
}
|
||||||
function relativeFromIso(iso: string): string {
|
function relativeFromIso(iso: string, t: (k: string, v?: Record<string, unknown>) => string): string {
|
||||||
const t = new Date(iso).getTime()
|
const ts = new Date(iso).getTime()
|
||||||
if (!t) return ''
|
if (!ts) return ''
|
||||||
return relativeTime(Math.floor(t / 1000))
|
return relativeTime(Math.floor(ts / 1000), t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Page ──────────────────────────────────────────────────────
|
// ── Page ──────────────────────────────────────────────────────
|
||||||
@@ -438,7 +438,7 @@ export default function DashboardPage() {
|
|||||||
<Tag color="blue" style={{ margin: 0, fontSize: 10 }}>{e.action}</Tag>
|
<Tag color="blue" style={{ margin: 0, fontSize: 10 }}>{e.action}</Tag>
|
||||||
<Text style={{ fontSize: 12 }}><b>{e.actor}</b></Text>
|
<Text style={{ fontSize: 12 }}><b>{e.actor}</b></Text>
|
||||||
{e.subject && <Text type="secondary" style={{ fontSize: 11 }}>{e.subject}</Text>}
|
{e.subject && <Text type="secondary" style={{ fontSize: 11 }}>{e.subject}</Text>}
|
||||||
<Text type="secondary" style={{ fontSize: 11, marginLeft: 'auto' }}>{relativeFromIso(e.created_at)}</Text>
|
<Text type="secondary" style={{ fontSize: 11, marginLeft: 'auto' }}>{relativeFromIso(e.created_at, t)}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -493,7 +493,7 @@ export default function DashboardPage() {
|
|||||||
{status.map(s => (
|
{status.map(s => (
|
||||||
<div key={s.peer_public_key} style={{ fontSize: 12, color: '#64748B', marginTop: 2 }}>
|
<div key={s.peer_public_key} style={{ fontSize: 12, color: '#64748B', marginTop: 2 }}>
|
||||||
{s.endpoint && <span>{s.endpoint} · </span>}
|
{s.endpoint && <span>{s.endpoint} · </span>}
|
||||||
<span>{relativeTime(s.last_handshake_unix)}</span>
|
<span>{relativeTime(s.last_handshake_unix, t)}</span>
|
||||||
{' · ▼'}{formatBytes(s.transfer_rx)}{' · ▲'}{formatBytes(s.transfer_tx)}
|
{' · ▼'}{formatBytes(s.transfer_rx)}{' · ▲'}{formatBytes(s.transfer_tx)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ function fmtBytes(n: number): string {
|
|||||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
||||||
return `${v.toFixed(1)} ${u[i]}`
|
return `${v.toFixed(1)} ${u[i]}`
|
||||||
}
|
}
|
||||||
function relTime(unix: number, never: string): string {
|
function relTime(unix: number, t: (k: string, v?: Record<string, unknown>) => string): string {
|
||||||
if (!unix) return never
|
if (!unix) return t('common.relTime.never')
|
||||||
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
||||||
if (sec < 60) return `${sec}s ago`
|
if (sec < 60) return t('common.relTime.Xs', { n: sec })
|
||||||
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`
|
if (sec < 3600) return t('common.relTime.Xm', { n: Math.floor(sec / 60) })
|
||||||
if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`
|
if (sec < 86400) return t('common.relTime.Xh', { n: Math.floor(sec / 3600) })
|
||||||
return `${Math.floor(sec / 86400)}d ago`
|
return t('common.relTime.Xd', { n: Math.floor(sec / 86400) })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientForm {
|
interface ClientForm {
|
||||||
@@ -145,7 +145,7 @@ export default function ClientsTab() {
|
|||||||
{live && (
|
{live && (
|
||||||
<Tooltip title={`▼${fmtBytes(live.transfer_rx)} ▲${fmtBytes(live.transfer_tx)}`}>
|
<Tooltip title={`▼${fmtBytes(live.transfer_rx)} ▲${fmtBytes(live.transfer_tx)}`}>
|
||||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
{relTime(live.last_handshake_unix, t('wg.peer.never'))}
|
{relTime(live.last_handshake_unix, t)}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ function fmtBytes(n: number): string {
|
|||||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
||||||
return `${v.toFixed(1)} ${u[i]}`
|
return `${v.toFixed(1)} ${u[i]}`
|
||||||
}
|
}
|
||||||
function relTime(unix: number, never: string): string {
|
function relTime(unix: number, t: (k: string, v?: Record<string, unknown>) => string): string {
|
||||||
if (!unix) return never
|
if (!unix) return t('common.relTime.never')
|
||||||
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
||||||
if (sec < 60) return `vor ${sec}s`
|
if (sec < 60) return t('common.relTime.Xs', { n: sec })
|
||||||
if (sec < 3600) return `vor ${Math.floor(sec / 60)}m`
|
if (sec < 3600) return t('common.relTime.Xm', { n: Math.floor(sec / 60) })
|
||||||
if (sec < 86400) return `vor ${Math.floor(sec / 3600)}h`
|
if (sec < 86400) return t('common.relTime.Xh', { n: Math.floor(sec / 3600) })
|
||||||
return `vor ${Math.floor(sec / 86400)}d`
|
return t('common.relTime.Xd', { n: Math.floor(sec / 86400) })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FwZoneLite { name: string; builtin: boolean }
|
interface FwZoneLite { name: string; builtin: boolean }
|
||||||
@@ -405,7 +405,7 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
|||||||
inactiveLabel={t('wg.peer.offline')}
|
inactiveLabel={t('wg.peer.offline')}
|
||||||
/>
|
/>
|
||||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
{live ? relTime(live.last_handshake_unix, t('wg.peer.never')) : t('wg.peer.never')}
|
{live ? relTime(live.last_handshake_unix, t) : t('common.relTime.never')}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user