Skip to content

Commit ec76f86

Browse files
committed
fix(v1.2.1): i18n translations for all pages, section headers, family group user picker
1 parent 9a3fe51 commit ec76f86

11 files changed

Lines changed: 90 additions & 70 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.0
1+
1.2.1

web/src/pages/Analytics.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
33
import { api } from "@/api/client";
44
import { Button, Card, PageHeader, Select } from "@/components/ui";
55
import { formatBytes } from "@/lib/utils";
6+
import { useI18n } from "@/i18n/i18n";
67

78
interface GeoPoint {
89
country: string;
@@ -32,6 +33,7 @@ interface AnalyticsData {
3233
}
3334

3435
export function Analytics() {
36+
const { t } = useI18n();
3537
const [range, setRange] = useState("7d");
3638
const rangeMap: Record<string, number> = { "1d": 1, "7d": 7, "30d": 30 };
3739

@@ -46,44 +48,44 @@ export function Analytics() {
4648
return (
4749
<div className="space-y-6 animate-fade-in">
4850
<div className="flex items-center justify-between">
49-
<PageHeader title="Analytics" subtitle="Traffic insights and geo breakdown" />
51+
<PageHeader title={t("analytics.title")} subtitle={t("analytics.subtitle")} />
5052
<div className="flex gap-2">
5153
<Select value={range} onChange={(e) => setRange(e.target.value)}>
5254
<option value="1d">Last 24h</option>
5355
<option value="7d">Last 7 days</option>
5456
<option value="30d">Last 30 days</option>
5557
</Select>
5658
<Button variant="outline" onClick={() => window.open(`/api/analytics/export?from=${from}&to=${to}`, "_blank")}>
57-
Export CSV
59+
{t("analytics.export")}
5860
</Button>
5961
</div>
6062
</div>
6163

62-
{isLoading && <div className="text-center text-fg-muted py-8">Loading analytics...</div>}
63-
{isError && <div className="text-center text-fg-muted py-8">Unable to load analytics data. Make sure the analytics feature is configured.</div>}
64-
{!isLoading && !isError && !data && <div className="text-center text-fg-muted py-8">No analytics data available for this time range.</div>}
64+
{isLoading && <div className="text-center text-fg-muted py-8">{t("common.loading")}</div>}
65+
{isError && <div className="text-center text-fg-muted py-8">{t("analytics.error")}</div>}
66+
{!isLoading && !isError && !data && <div className="text-center text-fg-muted py-8">{t("analytics.noData")}</div>}
6567

6668
{data && (
6769
<>
6870
{/* Summary cards */}
6971
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
7072
<Card className="space-y-2">
71-
<div className="text-xs text-fg-subtle uppercase">Total Upload</div>
73+
<div className="text-xs text-fg-subtle uppercase">{t("analytics.totalUp")}</div>
7274
<div className="text-lg font-bold text-fg">{formatBytes(data.total_up, false)}</div>
7375
</Card>
7476
<Card className="space-y-2">
75-
<div className="text-xs text-fg-subtle uppercase">Total Download</div>
77+
<div className="text-xs text-fg-subtle uppercase">{t("analytics.totalDown")}</div>
7678
<div className="text-lg font-bold text-fg">{formatBytes(data.total_down, false)}</div>
7779
</Card>
7880
<Card className="space-y-2">
79-
<div className="text-xs text-fg-subtle uppercase">Countries</div>
81+
<div className="text-xs text-fg-subtle uppercase">{t("analytics.countries")}</div>
8082
<div className="text-lg font-bold text-fg">{data.geo_breakdown?.length ?? 0}</div>
8183
</Card>
8284
</div>
8385

8486
{/* Geo breakdown table */}
8587
<Card>
86-
<h3 className="text-sm font-bold text-fg mb-3">Traffic by Country</h3>
88+
<h3 className="text-sm font-bold text-fg mb-3">{t("analytics.byCountry")}</h3>
8789
<div className="overflow-x-auto">
8890
<table className="w-full text-sm">
8991
<thead>
@@ -110,7 +112,7 @@ export function Analytics() {
110112

111113
{/* Top users */}
112114
<Card>
113-
<h3 className="text-sm font-bold text-fg mb-3">Top Users by Traffic</h3>
115+
<h3 className="text-sm font-bold text-fg mb-3">{t("analytics.topUsers")}</h3>
114116
<div className="overflow-x-auto">
115117
<table className="w-full text-sm">
116118
<thead>
@@ -135,7 +137,7 @@ export function Analytics() {
135137

136138
{/* Peak hours */}
137139
<Card>
138-
<h3 className="text-sm font-bold text-fg mb-3">Peak Hours</h3>
140+
<h3 className="text-sm font-bold text-fg mb-3">{t("analytics.peakHours")}</h3>
139141
<div className="flex items-end gap-1 h-32">
140142
{data.peak_hours?.map((p) => {
141143
const maxBytes = Math.max(...(data.peak_hours?.map(h => h.bytes_total) ?? [1]));

web/src/pages/DoHSettings.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
33
import { api } from "@/api/client";
44
import { Button, Card, Input, PageHeader, Badge } from "@/components/ui";
55
import { useToast } from "@/components/toast";
6+
import { useI18n } from "@/i18n/i18n";
67

78
interface DoHConfig {
89
enabled: boolean;
@@ -32,6 +33,7 @@ interface QueryLog {
3233
}
3334

3435
export function DoHSettings() {
36+
const { t } = useI18n();
3537
const qc = useQueryClient();
3638
const toast = useToast();
3739
const [form, setForm] = useState<DoHConfig | null>(null);
@@ -63,7 +65,7 @@ export function DoHSettings() {
6365

6466
return (
6567
<div className="space-y-6 animate-fade-in">
66-
<PageHeader title="DNS-over-HTTPS" subtitle="Built-in DoH/DoT server for user privacy" />
68+
<PageHeader title={t("doh.title")} subtitle={t("doh.subtitle")} />
6769

6870
<div className="rounded-lg border border-border/40 bg-surface-2/20 p-4 text-xs text-fg-muted space-y-2">
6971
<p className="font-medium text-fg text-sm">Built-in DNS Privacy Server</p>
@@ -79,19 +81,19 @@ export function DoHSettings() {
7981
{stats && (
8082
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
8183
<Card className="space-y-1">
82-
<div className="text-xs text-fg-subtle">Total Queries</div>
84+
<div className="text-xs text-fg-subtle">{t("doh.totalQueries")}</div>
8385
<div className="text-lg font-bold text-fg">{stats.total_queries.toLocaleString()}</div>
8486
</Card>
8587
<Card className="space-y-1">
86-
<div className="text-xs text-fg-subtle">Blocked</div>
88+
<div className="text-xs text-fg-subtle">{t("doh.blocked")}</div>
8789
<div className="text-lg font-bold text-danger">{stats.blocked_count.toLocaleString()}</div>
8890
</Card>
8991
<Card className="space-y-1">
90-
<div className="text-xs text-fg-subtle">Cache Hits</div>
92+
<div className="text-xs text-fg-subtle">{t("doh.cacheHits")}</div>
9193
<div className="text-lg font-bold text-success">{stats.cache_hits.toLocaleString()}</div>
9294
</Card>
9395
<Card className="space-y-1">
94-
<div className="text-xs text-fg-subtle">Avg Latency</div>
96+
<div className="text-xs text-fg-subtle">{t("doh.avgLatency")}</div>
9597
<div className="text-lg font-bold text-fg">{stats.avg_latency_ms}ms</div>
9698
</Card>
9799
</div>
@@ -100,15 +102,15 @@ export function DoHSettings() {
100102
{config && (
101103
<Card className="space-y-4">
102104
<div className="flex items-center justify-between">
103-
<h3 className="text-sm font-bold text-fg">DoH Configuration</h3>
105+
<h3 className="text-sm font-bold text-fg">{t("doh.config")}</h3>
104106
<label className="flex items-center gap-2 text-sm">
105107
<input type="checkbox" checked={config.enabled} onChange={(e) => update("enabled", e.target.checked)} className="rounded" />
106108
Enabled
107109
</label>
108110
</div>
109111
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
110112
<div>
111-
<label className="text-xs text-fg-subtle">Listen address</label>
113+
<label className="text-xs text-fg-subtle">{t("doh.listenAddr")}</label>
112114
<Input value={config.listen_addr} onChange={(e) => update("listen_addr", e.target.value)} placeholder=":8053" />
113115
</div>
114116
<div>
@@ -137,7 +139,7 @@ export function DoHSettings() {
137139

138140
{logsData?.logs && logsData.logs.length > 0 && (
139141
<Card>
140-
<h3 className="text-sm font-bold text-fg mb-3">Recent Queries</h3>
142+
<h3 className="text-sm font-bold text-fg mb-3">{t("doh.recentQueries")}</h3>
141143
<div className="overflow-x-auto max-h-[300px]">
142144
<table className="w-full text-xs">
143145
<thead>

web/src/pages/Evasion.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { api } from "@/api/client";
55
import { Button, Card, Input, PageHeader, Select } from "@/components/ui";
66
import { Modal } from "@/components/Modal";
77
import { useToast } from "@/components/toast";
8+
import { useI18n } from "@/i18n/i18n";
89

910
interface EvasionProfile {
1011
id: string;
@@ -20,6 +21,7 @@ interface EvasionProfile {
2021
}
2122

2223
export function Evasion() {
24+
const { t } = useI18n();
2325
const qc = useQueryClient();
2426
const [createOpen, setCreateOpen] = useState(false);
2527
const toast = useToast();
@@ -39,8 +41,8 @@ export function Evasion() {
3941
return (
4042
<div className="space-y-6 animate-fade-in">
4143
<div className="flex items-center justify-between">
42-
<PageHeader title="Evasion Profiles" />
43-
<Button onClick={() => setCreateOpen(true)}>New profile</Button>
44+
<PageHeader title={t("evasion.title")} />
45+
<Button onClick={() => setCreateOpen(true)}>{t("evasion.newProfile")}</Button>
4446
</div>
4547

4648
<div className="rounded-lg border border-border/40 bg-surface-2/20 p-4 text-xs text-fg-muted space-y-2">

web/src/pages/Federation.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { api } from "@/api/client";
44
import { Button, Card, Input, PageHeader, Badge } from "@/components/ui";
55
import { Modal } from "@/components/Modal";
66
import { useToast } from "@/components/toast";
7+
import { useI18n } from "@/i18n/i18n";
78

89
interface FedConfig { enabled: boolean; cluster_name: string; sso_enabled: boolean; sync_interval: number; }
910
interface Peer { id: string; name: string; endpoint: string; status: string; sync_users: boolean; sync_nodes: boolean; last_sync: string | null; }
1011
interface SyncEvent { id: string; peer_name: string; direction: string; entity_type: string; count: number; status: string; created_at: string; }
1112

1213
export function Federation() {
14+
const { t } = useI18n();
1315
const qc = useQueryClient(); const toast = useToast();
1416
const [addOpen, setAddOpen] = useState(false);
1517
const [form, setForm] = useState<FedConfig | null>(null);
@@ -26,7 +28,7 @@ export function Federation() {
2628

2729
return (
2830
<div className="space-y-6 animate-fade-in">
29-
<PageHeader title="Panel Federation" subtitle="Connect multiple VortexUI panels together" />
31+
<PageHeader title={t("federation.title")} subtitle={t("federation.subtitle")} />
3032

3133
<div className="rounded-lg border border-border/40 bg-surface-2/20 p-4 text-xs text-fg-muted space-y-2">
3234
<p className="font-medium text-fg text-sm">Multi-Panel Federation</p>
@@ -50,7 +52,7 @@ export function Federation() {
5052
)}
5153

5254
<Card>
53-
<div className="flex items-center justify-between mb-3"><h3 className="text-sm font-bold text-fg">Peers</h3><Button size="sm" onClick={() => setAddOpen(true)}>Add Peer</Button></div>
55+
<div className="flex items-center justify-between mb-3"><h3 className="text-sm font-bold text-fg">{t("federation.peers")}</h3><Button size="sm" onClick={() => setAddOpen(true)}>{t("federation.addPeer")}</Button></div>
5456
<AddPeerModal open={addOpen} onClose={() => setAddOpen(false)} />
5557
<div className="space-y-2">
5658
{peersData?.peers?.map(p => (
@@ -63,12 +65,12 @@ export function Federation() {
6365
<Button variant="ghost" size="sm" className="text-destructive" onClick={() => delPeer.mutate(p.id)}>Remove</Button>
6466
</div>
6567
))}
66-
{(!peersData?.peers || peersData.peers.length === 0) && <p className="text-xs text-fg-muted text-center py-4">No peers connected.</p>}
68+
{(!peersData?.peers || peersData.peers.length === 0) && <p className="text-xs text-fg-muted text-center py-4">{t("federation.noPeers")}</p>}
6769
</div>
6870
</Card>
6971

7072
<Card>
71-
<h3 className="text-sm font-bold text-fg mb-3">Sync Events</h3>
73+
<h3 className="text-sm font-bold text-fg mb-3">{t("federation.syncEvents")}</h3>
7274
<div className="space-y-2 max-h-[200px] overflow-y-auto">
7375
{eventsData?.events?.map(e => (
7476
<div key={e.id} className="flex items-center justify-between text-xs rounded-lg bg-surface-2/40 px-3 py-2">

web/src/pages/Fingerprint.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { api } from "@/api/client";
44
import { Button, Card, Input, PageHeader, Badge, Select } from "@/components/ui";
55
import { Modal } from "@/components/Modal";
66
import { useToast } from "@/components/toast";
7+
import { useI18n } from "@/i18n/i18n";
78

89
interface FPPolicy { enabled: boolean; default_action: string; log_unknown: boolean; }
910
interface FPRule { id: string; name: string; fingerprint: string; ja3_hash: string; action: string; priority: number; enabled: boolean; }
1011
interface FPEvent { id: string; client_ip: string; fingerprint: string; user_agent: string; action: string; created_at: string; }
1112

1213
export function Fingerprint() {
14+
const { t } = useI18n();
1315
const qc = useQueryClient(); const toast = useToast();
1416
const [addOpen, setAddOpen] = useState(false);
1517
const [form, setForm] = useState<FPPolicy | null>(null);
@@ -26,7 +28,7 @@ export function Fingerprint() {
2628

2729
return (
2830
<div className="space-y-6 animate-fade-in">
29-
<PageHeader title="Client Fingerprint" subtitle="Validate TLS client fingerprints to block suspicious connections" />
31+
<PageHeader title={t("fingerprint.title")} subtitle={t("fingerprint.subtitle")} />
3032

3133
<div className="rounded-lg border border-border/40 bg-surface-2/20 p-4 text-xs text-fg-muted space-y-2">
3234
<p className="font-medium text-fg text-sm">TLS Client Fingerprinting</p>
@@ -41,7 +43,7 @@ export function Fingerprint() {
4143
{policy && (
4244
<Card className="space-y-3">
4345
<div className="flex items-center justify-between">
44-
<h3 className="text-sm font-bold text-fg">Policy</h3>
46+
<h3 className="text-sm font-bold text-fg">{t("fingerprint.policy")}</h3>
4547
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={policy.enabled} onChange={e => update("enabled", e.target.checked)} className="rounded" /> Enabled</label>
4648
</div>
4749
<div className="grid grid-cols-2 gap-3">
@@ -52,7 +54,7 @@ export function Fingerprint() {
5254
</Card>
5355
)}
5456
<Card>
55-
<div className="flex items-center justify-between mb-3"><h3 className="text-sm font-bold text-fg">Rules</h3><Button size="sm" onClick={() => setAddOpen(true)}>Add Rule</Button></div>
57+
<div className="flex items-center justify-between mb-3"><h3 className="text-sm font-bold text-fg">{t("fingerprint.rules")}</h3><Button size="sm" onClick={() => setAddOpen(true)}>{t("fingerprint.addRule")}</Button></div>
5658
<AddRuleModal open={addOpen} onClose={() => setAddOpen(false)} />
5759
<div className="space-y-2">
5860
{rulesData?.rules?.map(r => (
@@ -61,11 +63,11 @@ export function Fingerprint() {
6163
<Button variant="ghost" size="sm" className="text-destructive" onClick={() => delRule.mutate(r.id)}>Del</Button>
6264
</div>
6365
))}
64-
{(!rulesData?.rules || rulesData.rules.length === 0) && <p className="text-xs text-fg-muted text-center py-4">No rules.</p>}
66+
{(!rulesData?.rules || rulesData.rules.length === 0) && <p className="text-xs text-fg-muted text-center py-4">{t("fingerprint.noRules")}</p>}
6567
</div>
6668
</Card>
6769
<Card>
68-
<h3 className="text-sm font-bold text-fg mb-3">Recent Events</h3>
70+
<h3 className="text-sm font-bold text-fg mb-3">{t("fingerprint.recentEvents")}</h3>
6971
<div className="space-y-2 max-h-[250px] overflow-y-auto">
7072
{eventsData?.events?.slice(0, 20).map(e => (
7173
<div key={e.id} className="flex items-center justify-between rounded-lg bg-surface-2/40 px-3 py-2 text-xs">

0 commit comments

Comments
 (0)