Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 23fdf24

Browse files
committed
feat(admin): i18n migration — extract hardcoded zh-CN strings via translate()
Move literal UI strings across all admin features into en-US/zh-CN resource bundles and route them through the new `~/i18n/translate` helper, enabling runtime locale switching via UI_LOCALE_STORAGE_KEY.
1 parent b08c824 commit 23fdf24

252 files changed

Lines changed: 9160 additions & 2585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/admin/src/api/auth.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TokenModel } from '~/models/token'
22

3+
import { translate } from '~/i18n/translate'
34
import { authClient } from '~/utils/authjs/auth'
45

56
import { getJson, postJson, requestJson } from './http'
@@ -52,7 +53,8 @@ export function authAsOwner() {
5253

5354
export async function listPasskeys() {
5455
const result = await authClient.passkey.listUserPasskeys()
55-
if (result.error) throw new Error(result.error.message || '获取 Passkey 失败')
56+
if (result.error)
57+
throw new Error(result.error.message || translate('api.error.passkeyFetch'))
5658

5759
return (result.data ?? []).map((passkey: any) => ({
5860
createdAt: String(passkey.createdAt ?? new Date().toISOString()),
@@ -65,5 +67,8 @@ export async function listPasskeys() {
6567

6668
export async function deletePasskey(id: string) {
6769
const result = await authClient.passkey.deletePasskey({ id })
68-
if (result.error) throw new Error(result.error.message || '删除 Passkey 失败')
70+
if (result.error)
71+
throw new Error(
72+
result.error.message || translate('api.error.passkeyDelete'),
73+
)
6974
}

apps/admin/src/api/dependencies.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { API_URL } from '../constants/env'
2+
import { translate } from '../i18n/translate'
23
import { getJson } from './http'
34

45
export interface DependencyGraph {
@@ -36,7 +37,7 @@ export async function getNpmPackageLatest(name: string) {
3637
)
3738

3839
if (!response.ok) {
39-
throw new Error(`获取 ${name} 最新版本失败`)
40+
throw new Error(translate('api.error.npmLatest', { name }))
4041
}
4142

4243
return (await response.json()) as NpmPackageLatest

apps/admin/src/api/files.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { API_URL } from '~/constants/env'
2+
import { translate } from '~/i18n/translate'
23

34
import { deleteJson, getJson, patchJson, requestJson } from './http'
45

@@ -129,7 +130,7 @@ export function uploadFileWithProgress(
129130
resolve(readUploadResponse(responseData))
130131
}
131132

132-
xhr.onerror = () => reject(new Error('上传失败'))
133+
xhr.onerror = () => reject(new Error(translate('api.error.uploadFailed')))
133134
xhr.send(formData)
134135
})
135136
}

apps/admin/src/api/github-repo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { translate } from '../i18n/translate'
2+
13
const endpoint = 'https://api.github.com/'
24

35
export interface GithubRepo {
@@ -13,7 +15,7 @@ interface GithubReadme {
1315

1416
export async function getRepoDetail(owner: string, repo: string) {
1517
const response = await fetch(`${endpoint}repos/${owner}/${repo}`)
16-
if (!response.ok) throw new Error('获取 GitHub 仓库信息失败')
18+
if (!response.ok) throw new Error(translate('api.error.githubRepo'))
1719

1820
return response.json() as Promise<GithubRepo>
1921
}

apps/admin/src/api/github-snippets.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { translate } from '../i18n/translate'
2+
13
interface GitHubContentItem {
24
download_url?: string | null
35
html_url?: string | null
@@ -15,7 +17,7 @@ export async function fetchGitHubSnippetTree(path = '') {
1517
const response = await fetch(target)
1618

1719
if (!response.ok) {
18-
throw new Error('获取 GitHub Snippets 仓库失败')
20+
throw new Error(translate('api.error.githubSnippets'))
1921
}
2022

2123
return (await response.json()) as GitHubContentItem[] | GitHubContentItem
@@ -25,7 +27,7 @@ export async function fetchGitHubText(downloadUrl: string) {
2527
const response = await fetch(downloadUrl)
2628

2729
if (!response.ok) {
28-
throw new Error('获取文件内容失败')
30+
throw new Error(translate('api.error.fetchFile'))
2931
}
3032

3133
return response.text()

apps/admin/src/features/_shared/components/content-list-item.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
Ref,
1010
} from 'react'
1111

12+
import { useI18n } from '~/i18n'
1213
import { ContextMenuTrigger, showContextMenu } from '~/ui/overlay/context-menu'
1314
import { Checkbox } from '~/ui/primitives/checkbox'
1415
import { cn } from '~/utils/cn'
@@ -54,6 +55,7 @@ const INTERACTIVE_SELECTOR =
5455
* single / toggle (`$mod`+click) / range (`Shift`+click) selection.
5556
*/
5657
export function ContentEntryListItem(props: ContentEntryListItemProps) {
58+
const { t } = useI18n()
5759
const selectable = Boolean(props.checkboxLabel && props.onSelectedChange)
5860
const handleSelectedChange = props.onSelectedChange ?? (() => {})
5961

@@ -154,7 +156,10 @@ export function ContentEntryListItem(props: ContentEntryListItemProps) {
154156
>
155157
<ExternalLink aria-hidden="true" className="size-4" />
156158
</ActionLink>
157-
<ActionButton onClick={onMoreClick} title="更多操作">
159+
<ActionButton
160+
onClick={onMoreClick}
161+
title={t('shared.contentListItem.moreActions')}
162+
>
158163
<MoreHorizontal aria-hidden="true" className="size-4" />
159164
</ActionButton>
160165
</div>

apps/admin/src/features/_shared/components/content-list-toolbar.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect } from 'react'
44
import type { FormEventHandler, ReactNode } from 'react'
55

66
import { APP_SHELL_HEADER_HEIGHT_CLASS } from '~/constants/layout'
7+
import { useI18n } from '~/i18n'
78
import { PortalLayerScope, useFloatingZ } from '~/ui/feedback/portal-layer'
89
import { MobileHamburger } from '~/ui/layout/mobile-hamburger'
910
import { useShellNav } from '~/ui/layout/shell-nav-context'
@@ -80,6 +81,7 @@ export function ContentListHeader(props: {
8081
}
8182

8283
export function ContentListToolbar(props: ContentListToolbarProps) {
84+
const { t } = useI18n()
8385
const selection = props.selection
8486
const selectedCount = selection?.selectedCount ?? 0
8587
const hasSelection = selectedCount > 0
@@ -111,7 +113,7 @@ export function ContentListToolbar(props: ContentListToolbarProps) {
111113
/>
112114
{props.hasSearch ? (
113115
<button
114-
aria-label="清除搜索"
116+
aria-label={t('shared.contentListToolbar.clearSearch')}
115117
className="absolute right-3 top-1/2 inline-flex size-4 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
116118
onClick={props.onClearSearch}
117119
type="button"
@@ -211,6 +213,7 @@ interface SortMenuProps<TField extends string = string> {
211213
export function SortMenu<TField extends string = string>(
212214
props: SortMenuProps<TField>,
213215
) {
216+
const { t } = useI18n()
214217
const activeOption = props.options.find(
215218
(option) => option.value === props.field,
216219
)
@@ -220,7 +223,7 @@ export function SortMenu<TField extends string = string>(
220223
return (
221224
<Popover.Root>
222225
<Popover.Trigger
223-
aria-label="排序"
226+
aria-label={t('shared.sortMenu.label')}
224227
className={cn(
225228
'outline-hidden inline-flex h-7 shrink-0 items-center gap-1.5 rounded px-2 text-xs text-neutral-700 transition-colors hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-[var(--color-primary-shallow)] disabled:cursor-not-allowed disabled:opacity-60 data-[popup-open]:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-900 dark:data-[popup-open]:bg-neutral-900',
226229
props.className,
@@ -229,7 +232,9 @@ export function SortMenu<TField extends string = string>(
229232
type="button"
230233
>
231234
<ArrowUpDown aria-hidden="true" className="size-3.5 text-neutral-400" />
232-
<span className="truncate">{activeOption?.label ?? '排序'}</span>
235+
<span className="truncate">
236+
{activeOption?.label ?? t('shared.sortMenu.label')}
237+
</span>
233238
<OrderIcon aria-hidden="true" className="size-3 text-neutral-400" />
234239
</Popover.Trigger>
235240
<Popover.Portal>
@@ -242,7 +247,7 @@ export function SortMenu<TField extends string = string>(
242247
<PortalLayerScope depth={depth}>
243248
<Popover.Popup className="outline-hidden w-48 rounded border border-neutral-200 bg-white p-1 text-xs shadow-lg dark:border-neutral-800 dark:bg-neutral-950">
244249
<div className="px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
245-
字段
250+
{t('shared.sortMenu.field')}
246251
</div>
247252
{props.options.map((option) => {
248253
const active = option.value === props.field
@@ -272,21 +277,21 @@ export function SortMenu<TField extends string = string>(
272277
})}
273278
<div className="mx-1 my-1 border-t border-neutral-100 dark:border-neutral-800" />
274279
<div className="px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
275-
方向
280+
{t('shared.sortMenu.direction')}
276281
</div>
277282
<div className="grid grid-cols-2 gap-1 p-1">
278283
<SortOrderButton
279284
active={props.order === 'desc'}
280285
icon={<ArrowDown aria-hidden="true" className="size-3.5" />}
281-
label="降序"
286+
label={t('shared.sortMenu.desc')}
282287
onClick={() =>
283288
props.onChange({ field: props.field, order: 'desc' })
284289
}
285290
/>
286291
<SortOrderButton
287292
active={props.order === 'asc'}
288293
icon={<ArrowUp aria-hidden="true" className="size-3.5" />}
289-
label="升序"
294+
label={t('shared.sortMenu.asc')}
290295
onClick={() =>
291296
props.onChange({ field: props.field, order: 'asc' })
292297
}

apps/admin/src/features/_shared/components/ip-info-popover.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from 'react'
44
import type { ReactNode } from 'react'
55

66
import { callBuiltInFunction } from '~/api/system'
7+
import { useI18n } from '~/i18n'
78
import { PortalLayerScope, useFloatingZ } from '~/ui/feedback/portal-layer'
89

910
interface IpInfo {
@@ -26,6 +27,7 @@ export function IpInfoPopover(props: {
2627
ip: string
2728
trigger?: ReactNode
2829
}) {
30+
const { t } = useI18n()
2931
const [info, setInfo] = useState<IpInfo | null>(
3032
() => ipInfoCache.get(props.ip) ?? null,
3133
)
@@ -47,7 +49,11 @@ export function IpInfoPopover(props: {
4749
ipInfoCache.set(props.ip, result)
4850
setInfo(result)
4951
} catch (loadError) {
50-
setError(loadError instanceof Error ? loadError.message : '获取失败')
52+
setError(
53+
loadError instanceof Error
54+
? loadError.message
55+
: t('shared.ipInfo.fetchFailed'),
56+
)
5157
} finally {
5258
setLoading(false)
5359
}
@@ -86,24 +92,32 @@ export function IpInfoPopover(props: {
8692
<PortalLayerScope depth={depth}>
8793
<Popover.Popup className="outline-hidden w-72 rounded border border-neutral-200 bg-white p-3 text-xs text-neutral-600 shadow-xl dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-300">
8894
{loading ? (
89-
<span className="text-neutral-400">获取中...</span>
95+
<span className="text-neutral-400">
96+
{t('shared.ipInfo.loading')}
97+
</span>
9098
) : error ? (
9199
<span className="text-red-500">{error}</span>
92100
) : info ? (
93101
<div className="grid gap-2">
94102
<InfoRow label="IP" value={info.ip || props.ip} />
95103
<InfoRow
96-
label="城市"
104+
label={t('shared.ipInfo.label.city')}
97105
value={
98106
[info.countryName, info.regionName, info.cityName]
99107
.filter(Boolean)
100108
.join(' - ') || 'N/A'
101109
}
102110
/>
103-
<InfoRow label="ISP" value={info.ispDomain || 'N/A'} />
104-
<InfoRow label="组织" value={info.ownerDomain || 'N/A'} />
105111
<InfoRow
106-
label="范围"
112+
label={t('shared.ipInfo.label.isp')}
113+
value={info.ispDomain || 'N/A'}
114+
/>
115+
<InfoRow
116+
label={t('shared.ipInfo.label.org')}
117+
value={info.ownerDomain || 'N/A'}
118+
/>
119+
<InfoRow
120+
label={t('shared.ipInfo.label.range')}
107121
value={
108122
info.range
109123
? [info.range.from, info.range.to]
@@ -114,7 +128,9 @@ export function IpInfoPopover(props: {
114128
/>
115129
</div>
116130
) : (
117-
<span className="text-neutral-400">暂无信息</span>
131+
<span className="text-neutral-400">
132+
{t('shared.ipInfo.empty')}
133+
</span>
118134
)}
119135
</Popover.Popup>
120136
</PortalLayerScope>

0 commit comments

Comments
 (0)