Skip to content

Commit 97085ef

Browse files
author
Developer
committed
fix: add i18n to all pages and fix host.docker.internal DNS in Docker
- Replace all hardcoded English text across PITR, Agents, Audit, and Org pages with t() translation calls. Default UI language is Chinese, switches to English when selected. - Add extra_hosts to server service in docker-compose.yml so that host.docker.internal resolves correctly on Linux/WSL2 Docker environments, allowing PITR recovery to connect to MySQL on the host machine. - Add 20+ missing i18n translation keys to zh.json and en.json.
1 parent 8813e84 commit 97085ef

12 files changed

Lines changed: 138 additions & 78 deletions

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ services:
4444
environment:
4545
DATABASE_URL: "root:pitr_test@tcp(mysql:3306)/pitr_server"
4646
LISTEN_ADDR: ":8080"
47+
extra_hosts:
48+
- "host.docker.internal:host-gateway"
4749
depends_on:
4850
mysql:
4951
condition: service_healthy

web/src/locales/en.json

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@
5858
"created": "Organization created",
5959
"createFailed": "Failed to create organization",
6060
"invited": "Member invited",
61-
"inviteFailed": "Failed to invite member"
61+
"inviteFailed": "Failed to invite member",
62+
"member": "Member",
63+
"admin": "Admin",
64+
"settingsTitle": "Organization Settings",
65+
"generalSettings": "General Settings",
66+
"settingsDesc": "Organization settings will be available here. Admin access is required to modify settings."
6267
},
6368
"agents": {
6469
"title": "Agents",
@@ -89,7 +94,17 @@
8994
"viewDetail": "View Details",
9095
"delete": "Delete",
9196
"detail": "Agent Details",
92-
"hostnameRequired": "Please enter the hostname"
97+
"hostnameRequired": "Please enter the hostname",
98+
"hostnamePlaceholder": "e.g. db-server-01",
99+
"mysqlVersionPlaceholder": "e.g. 8.0.32",
100+
"id": "ID",
101+
"back": "Back",
102+
"backToList": "Back to Agents",
103+
"approve": "Approve Agent",
104+
"approveSuccess": "Agent approved",
105+
"approveFailed": "Failed to approve agent",
106+
"rejectConfirmDetail": "Are you sure you want to reject this agent?",
107+
"loadDetailFailed": "Failed to load agent details"
93108
},
94109
"pitr": {
95110
"title": "PITR Recovery",
@@ -151,7 +166,15 @@
151166
"viewAuditLog": "View Audit Log",
152167
"mysqlDsn": "MySQL DSN",
153168
"mysqlDsnHelp": "Format: user:password@tcp(host:port)/database",
154-
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database"
169+
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database",
170+
"targetTablePlaceholder": "e.g. mydb.orders",
171+
"calculating": "Calculating...",
172+
"updatedAt": "Updated At",
173+
"loadFailed": "Failed to load PITR operations",
174+
"noOperations": "No PITR operations yet",
175+
"noOperationsFiltered": "No PITR operations found in this time range",
176+
"operationId": "Operation ID",
177+
"agentId": "Agent ID"
155178
},
156179
"audit": {
157180
"title": "Audit Log",
@@ -194,6 +217,7 @@
194217
"common": {
195218
"loading": "Loading...",
196219
"error": "Error",
220+
"success": "Success",
197221
"retry": "Retry",
198222
"cancel": "Cancel",
199223
"confirm": "Confirm",

web/src/locales/zh.json

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@
5858
"created": "组织创建成功",
5959
"createFailed": "创建组织失败",
6060
"invited": "成员邀请成功",
61-
"inviteFailed": "邀请成员失败"
61+
"inviteFailed": "邀请成员失败",
62+
"member": "成员",
63+
"admin": "管理员",
64+
"settingsTitle": "组织设置",
65+
"generalSettings": "通用设置",
66+
"settingsDesc": "组织设置将在此处提供。需要管理员权限才能修改。"
6267
},
6368
"agents": {
6469
"title": "代理",
@@ -89,7 +94,17 @@
8994
"viewDetail": "查看详情",
9095
"delete": "删除",
9196
"detail": "代理详情",
92-
"hostnameRequired": "请输入主机名"
97+
"hostnameRequired": "请输入主机名",
98+
"hostnamePlaceholder": "例如 db-server-01",
99+
"mysqlVersionPlaceholder": "例如 8.0.32",
100+
"id": "ID",
101+
"back": "返回",
102+
"backToList": "返回代理列表",
103+
"approve": "批准代理",
104+
"approveSuccess": "代理已批准",
105+
"approveFailed": "批准代理失败",
106+
"rejectConfirmDetail": "确定要拒绝该代理吗?",
107+
"loadDetailFailed": "加载代理详情失败"
93108
},
94109
"pitr": {
95110
"title": "PITR 恢复",
@@ -151,7 +166,15 @@
151166
"viewAuditLog": "查看审计日志",
152167
"mysqlDsn": "MySQL 连接串",
153168
"mysqlDsnHelp": "格式:user:password@tcp(host:port)/database",
154-
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database"
169+
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database",
170+
"targetTablePlaceholder": "例如 mydb.orders",
171+
"calculating": "计算中...",
172+
"updatedAt": "更新时间",
173+
"loadFailed": "加载 PITR 操作失败",
174+
"noOperations": "暂无 PITR 恢复操作",
175+
"noOperationsFiltered": "该时间范围内暂无 PITR 恢复操作",
176+
"operationId": "操作 ID",
177+
"agentId": "代理 ID"
155178
},
156179
"audit": {
157180
"title": "审计日志",
@@ -194,6 +217,7 @@
194217
"common": {
195218
"loading": "加载中...",
196219
"error": "出错了",
220+
"success": "操作成功",
197221
"retry": "重试",
198222
"cancel": "取消",
199223
"confirm": "确认",

web/src/pages/agents/AgentDetailPage.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Card, Descriptions, Badge, Button, Spin, Typography, message, Space, Mo
44
import { ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
55
import dayjs from 'dayjs';
66
import { getAgent, approveAgent, rejectAgent } from '../../api/agents';
7+
import { useLocale } from '../../hooks/useLocale';
78

89
const { Title } = Typography;
910

@@ -17,6 +18,7 @@ export default function AgentDetailPage() {
1718
const { id } = useParams<{ id: string }>();
1819
const navigate = useNavigate();
1920
const queryClient = useQueryClient();
21+
const { t } = useLocale();
2022

2123
const { data: agent, isLoading, error } = useQuery({
2224
queryKey: ['agent', id],
@@ -27,32 +29,32 @@ export default function AgentDetailPage() {
2729
const approveMutation = useMutation({
2830
mutationFn: () => approveAgent(id!),
2931
onSuccess: () => {
30-
message.success('Agent approved');
32+
message.success(t('agents.approveSuccess'));
3133
queryClient.invalidateQueries({ queryKey: ['agent', id] });
3234
},
3335
onError: () => {
34-
message.error('Failed to approve agent');
36+
message.error(t('agents.approveFailed'));
3537
},
3638
});
3739

3840
const rejectMutation = useMutation({
3941
mutationFn: () => rejectAgent(id!),
4042
onSuccess: () => {
41-
message.success('Agent rejected');
43+
message.success(t('agents.rejected'));
4244
navigate('/agents');
4345
},
4446
onError: () => {
45-
message.error('Failed to reject agent');
47+
message.error(t('agents.rejectFailed'));
4648
},
4749
});
4850

4951
const showRejectConfirm = () => {
5052
Modal.confirm({
51-
title: 'Reject Agent',
52-
content: 'Are you sure you want to reject this agent?',
53-
okText: 'Confirm',
53+
title: t('agents.rejectTitle'),
54+
content: t('agents.rejectConfirmDetail'),
55+
okText: t('common.confirm'),
5456
okType: 'danger',
55-
cancelText: 'Cancel',
57+
cancelText: t('common.cancel'),
5658
onOk: () => rejectMutation.mutate(),
5759
});
5860
};
@@ -65,9 +67,9 @@ export default function AgentDetailPage() {
6567
return (
6668
<Card>
6769
<div style={{ textAlign: 'center', padding: 48 }}>
68-
<Typography.Text type="danger">Failed to load agent details.</Typography.Text>
70+
<Typography.Text type="danger">{t('agents.loadDetailFailed')}</Typography.Text>
6971
<br /><br />
70-
<Button onClick={() => navigate('/agents')}>Back to Agents</Button>
72+
<Button onClick={() => navigate('/agents')}>{t('agents.backToList')}</Button>
7173
</div>
7274
</Card>
7375
);
@@ -77,7 +79,7 @@ export default function AgentDetailPage() {
7779
<div>
7880
<Space style={{ marginBottom: 16 }}>
7981
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/agents')}>
80-
Back
82+
{t('agents.back')}
8183
</Button>
8284
</Space>
8385

@@ -94,7 +96,7 @@ export default function AgentDetailPage() {
9496
onClick={() => approveMutation.mutate()}
9597
loading={approveMutation.isPending}
9698
>
97-
Approve Agent
99+
{t('agents.approve')}
98100
</Button>
99101
{!agent.approved && (
100102
<Button
@@ -103,25 +105,25 @@ export default function AgentDetailPage() {
103105
onClick={showRejectConfirm}
104106
loading={rejectMutation.isPending}
105107
>
106-
Reject Agent
108+
{t('agents.rejectTitle')}
107109
</Button>
108110
)}
109111
</Space>
110112
</div>
111113

112114
<Descriptions bordered column={1}>
113-
<Descriptions.Item label="ID">{agent.id}</Descriptions.Item>
114-
<Descriptions.Item label="Hostname">{agent.hostname}</Descriptions.Item>
115-
<Descriptions.Item label="MySQL Version">
115+
<Descriptions.Item label={t('agents.id')}>{agent.id}</Descriptions.Item>
116+
<Descriptions.Item label={t('agents.hostname')}>{agent.hostname}</Descriptions.Item>
117+
<Descriptions.Item label={t('agents.mysqlVersion')}>
116118
{agent.mySQLVersion || '-'}
117119
</Descriptions.Item>
118-
<Descriptions.Item label="Status">
120+
<Descriptions.Item label={t('agents.status')}>
119121
<Badge status={statusBadge[agent.status]} text={agent.status} />
120122
</Descriptions.Item>
121-
<Descriptions.Item label="Last Seen">
123+
<Descriptions.Item label={t('agents.lastSeen')}>
122124
{agent.lastSeen ? dayjs(agent.lastSeen).format('YYYY-MM-DD HH:mm:ss') : '-'}
123125
</Descriptions.Item>
124-
<Descriptions.Item label="Created At">
126+
<Descriptions.Item label={t('agents.created')}>
125127
{dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')}
126128
</Descriptions.Item>
127129
</Descriptions>

web/src/pages/agents/AgentListPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,13 @@ export default function AgentListPage() {
271271
label={t('agents.hostname')}
272272
rules={[{ required: true, message: t('agents.hostnameRequired') }]}
273273
>
274-
<Input placeholder="e.g. db-server-01" />
274+
<Input placeholder={t('agents.hostnamePlaceholder')} />
275275
</Form.Item>
276276
<Form.Item
277277
name="mySQLVersion"
278278
label={t('agents.mysqlVersion')}
279279
>
280-
<Input placeholder="e.g. 8.0.32" />
280+
<Input placeholder={t('agents.mysqlVersionPlaceholder')} />
281281
</Form.Item>
282282
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
283283
<Space>
@@ -305,7 +305,7 @@ export default function AgentListPage() {
305305
>
306306
{detailAgent && (
307307
<Descriptions bordered column={1} size="small">
308-
<Descriptions.Item label="ID">{detailAgent.id}</Descriptions.Item>
308+
<Descriptions.Item label={t('agents.id')}>{detailAgent.id}</Descriptions.Item>
309309
<Descriptions.Item label={t('agents.hostname')}>{detailAgent.hostname}</Descriptions.Item>
310310
<Descriptions.Item label={t('agents.mysqlVersion')}>
311311
{detailAgent.mySQLVersion || '-'}

web/src/pages/audit/AuditDetailPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function AuditDetailPage() {
5454
<div>
5555
<Space style={{ marginBottom: 16 }}>
5656
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/audit')}>
57-
Back
57+
{t('audit.back')}
5858
</Button>
5959
</Space>
6060

web/src/pages/audit/AuditLogPage.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default function AuditLogPage() {
102102
// Table columns
103103
const columns: ColumnsType<AuditEntry> = [
104104
{
105-
title: 'Operation ID',
105+
title: t('audit.operationId'),
106106
dataIndex: 'operationId',
107107
key: 'operationId',
108108
width: 120,
@@ -112,13 +112,13 @@ export default function AuditLogPage() {
112112
),
113113
},
114114
{
115-
title: 'Operator',
115+
title: t('audit.operator'),
116116
dataIndex: 'operator',
117117
key: 'operator',
118118
width: 160,
119119
},
120120
{
121-
title: 'Timestamp',
121+
title: t('audit.timestamp'),
122122
dataIndex: 'timestamp',
123123
key: 'timestamp',
124124
width: 180,
@@ -128,20 +128,20 @@ export default function AuditLogPage() {
128128
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
129129
},
130130
{
131-
title: 'Target Table',
131+
title: t('audit.targetTable'),
132132
dataIndex: 'targetTable',
133133
key: 'targetTable',
134134
width: 180,
135135
},
136136
{
137-
title: 'Rows Affected',
137+
title: t('audit.rowsAffected'),
138138
dataIndex: 'rowsAffected',
139139
key: 'rowsAffected',
140140
width: 120,
141141
render: (val: number) => val?.toLocaleString() || '0',
142142
},
143143
{
144-
title: 'Status',
144+
title: t('audit.status'),
145145
dataIndex: 'status',
146146
key: 'status',
147147
width: 120,
@@ -180,7 +180,7 @@ export default function AuditLogPage() {
180180
<Title level={3} style={{ margin: 0 }}>{t('audit.title')}</Title>
181181
<Space>
182182
<Button icon={<DownloadOutlined />} onClick={handleExport}>
183-
Export CSV
183+
{t('audit.export')}
184184
</Button>
185185
</Space>
186186
</div>
@@ -227,7 +227,7 @@ export default function AuditLogPage() {
227227
</Select>
228228
</div>
229229
<Button icon={<ReloadOutlined />} onClick={() => auditQuery.refetch()}>
230-
Refresh
230+
{t('audit.refresh')}
231231
</Button>
232232
{(dateRange || statusFilter || agentFilter) && (
233233
<Button onClick={clearFilters}>{t('audit.clearFilters')}</Button>
@@ -262,7 +262,7 @@ export default function AuditLogPage() {
262262
<Button onClick={clearFilters}>{t('audit.clearFilters')}</Button>
263263
)}
264264
<Button type="primary" onClick={() => navigate('/pitr/new')}>
265-
Start New Recovery
265+
{t('audit.startNewRecovery')}
266266
</Button>
267267
</Space>
268268
</Empty>

web/src/pages/org/OrgPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function OrgPage() {
2020

2121
const memberColumns = [
2222
{
23-
title: 'Email',
23+
title: t('auth.email'),
2424
dataIndex: 'email',
2525
key: 'email',
2626
},
@@ -189,18 +189,18 @@ export default function OrgPage() {
189189
<Form form={inviteForm} onFinish={handleInviteMember} layout="vertical">
190190
<Form.Item
191191
name="email"
192-
label="Email"
192+
label={t('auth.email')}
193193
rules={[
194194
{ required: true, message: t('auth.validation.emailRequired') },
195195
{ type: 'email', message: t('auth.validation.emailInvalid') },
196196
]}
197197
>
198-
<Input placeholder="user@example.com" />
198+
<Input placeholder={t('auth.placeholder.email')} />
199199
</Form.Item>
200200
<Form.Item name="role" label={t('org.role')} initialValue="member">
201201
<Select>
202-
<Select.Option value="member">Member</Select.Option>
203-
<Select.Option value="admin">Admin</Select.Option>
202+
<Select.Option value="member">{t('org.member')}</Select.Option>
203+
<Select.Option value="admin">{t('org.admin')}</Select.Option>
204204
</Select>
205205
</Form.Item>
206206
</Form>

0 commit comments

Comments
 (0)