folder页面全量多语言处理

This commit is contained in:
xintaofei
2026-03-07 13:44:40 +08:00
parent 3ddc8f165a
commit a356b813a6
10 changed files with 1533 additions and 343 deletions

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Check, ChevronDown, ChevronRight, Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Textarea } from "@/components/ui/textarea"
@@ -204,6 +205,8 @@ export function CommitWorkspace({
onCommitted,
onCancel,
}: CommitWorkspaceProps) {
const t = useTranslations("Folder.commitDialog")
const tCommon = useTranslations("Folder.common")
const [entries, setEntries] = useState<GitStatusEntry[]>([])
const containerRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState(0)
@@ -416,8 +419,10 @@ export function CommitWorkspace({
commitMessage,
Array.from(selected)
)
toast.success("提交代码完成", {
description: `已提交 ${result.committed_files} 个文件`,
toast.success(t("toasts.commitCompleted"), {
description: t("toasts.committedFiles", {
count: result.committed_files,
}),
})
onCommitted?.()
} catch (err) {
@@ -425,7 +430,7 @@ export function CommitWorkspace({
} finally {
setCommitting(false)
}
}, [folderPath, onCommitted, selected])
}, [folderPath, onCommitted, selected, t])
// --- Context menu actions ---
@@ -434,28 +439,28 @@ export function CommitWorkspace({
if (!folderPath) return
try {
await gitAddFiles(folderPath, [file])
toast.success("已添加到 VCS", { description: file })
toast.success(t("toasts.addedToVcs"), { description: file })
void loadStatus()
} catch (err) {
toast.error("添加到 VCS 失败", { description: String(err) })
toast.error(t("toasts.addToVcsFailed"), { description: String(err) })
}
},
[folderPath, loadStatus]
[folderPath, loadStatus, t]
)
const handleDeleteFile = useCallback(
(file: string) => {
setConfirm({
open: true,
title: "确认删除",
description: `确定要删除文件「${file}」吗?此操作不可恢复。`,
title: t("confirm.deleteTitle"),
description: t("confirm.deleteDescription", { file }),
variant: "destructive",
action: () => {
void (async () => {
if (!folderPath) return
try {
await deleteFileTreeEntry(folderPath, file)
toast.success("文件已删除", { description: file })
toast.success(t("toasts.fileDeleted"), { description: file })
// If deleted file was being viewed, clear the diff
if (diffFileRef.current === file) {
setDiffFile(null)
@@ -470,28 +475,30 @@ export function CommitWorkspace({
})
void loadStatus()
} catch (err) {
toast.error("删除失败", { description: String(err) })
toast.error(t("toasts.deleteFailed"), {
description: String(err),
})
}
})()
},
})
},
[folderPath, loadStatus]
[folderPath, loadStatus, t]
)
const handleRollbackFile = useCallback(
(file: string) => {
setConfirm({
open: true,
title: "确认回滚",
description: `确定要回滚文件「${file}」到 HEAD 版本吗?未保存的修改将丢失。`,
title: t("confirm.rollbackTitle"),
description: t("confirm.rollbackDescription", { file }),
variant: "destructive",
action: () => {
void (async () => {
if (!folderPath) return
try {
await gitRollbackFile(folderPath, file)
toast.success("文件已回滚", { description: file })
toast.success(t("toasts.fileRolledBack"), { description: file })
if (diffFileRef.current === file) {
setDiffFile(null)
setDiffOriginal("")
@@ -505,13 +512,15 @@ export function CommitWorkspace({
})
void loadStatus()
} catch (err) {
toast.error("回滚失败", { description: String(err) })
toast.error(t("toasts.rollbackFailed"), {
description: String(err),
})
}
})()
},
})
},
[folderPath, loadStatus]
[folderPath, loadStatus, t]
)
const closeConfirm = useCallback(() => {
@@ -631,7 +640,12 @@ export function CommitWorkspace({
? "border-primary bg-primary text-primary-foreground"
: "border-input"
)}
aria-label={`${selected.has(node.path) ? "取消选择" : "选择"} ${node.path}`}
aria-label={t("aria.selectFile", {
action: selected.has(node.path)
? t("actions.unselect")
: t("actions.select"),
path: node.path,
})}
>
{selected.has(node.path) && <Check className="h-3 w-3" />}
</button>
@@ -654,20 +668,28 @@ export function CommitWorkspace({
<ContextMenuContent>
{!isDeleted && (
<ContextMenuItem onClick={() => handleRollbackFile(node.path)}>
{t("actions.rollback")}
</ContextMenuItem>
)}
<ContextMenuItem
variant="destructive"
onClick={() => handleDeleteFile(node.path)}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
},
[selected, toggleFile, handleViewDiff, handleRollbackFile, handleDeleteFile]
[
selected,
toggleFile,
handleViewDiff,
handleRollbackFile,
handleDeleteFile,
t,
tCommon,
]
)
const renderUntrackedNode = useCallback(
@@ -704,7 +726,12 @@ export function CommitWorkspace({
? "border-primary bg-primary text-primary-foreground"
: "border-input"
)}
aria-label={`${selected.has(node.path) ? "取消选择" : "选择"} ${node.path}`}
aria-label={t("aria.selectFile", {
action: selected.has(node.path)
? t("actions.unselect")
: t("actions.select"),
path: node.path,
})}
>
{selected.has(node.path) && <Check className="h-3 w-3" />}
</button>
@@ -723,20 +750,28 @@ export function CommitWorkspace({
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleAddToVcs(node.path)}>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onClick={() => handleDeleteFile(node.path)}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
},
[selected, toggleFile, handleViewDiff, handleAddToVcs, handleDeleteFile]
[
selected,
toggleFile,
handleViewDiff,
handleAddToVcs,
handleDeleteFile,
t,
tCommon,
]
)
const toggleTrackedGroup = useCallback(
@@ -783,14 +818,21 @@ export function CommitWorkspace({
? "border-primary bg-primary text-primary-foreground"
: "border-input"
)}
aria-label={allSelected ? "取消选择全部文件" : "选择全部文件"}
aria-label={
allSelected
? t("aria.unselectAllFiles")
: t("aria.selectAllFiles")
}
>
{allSelected && <Check className="h-3 w-3" />}
</button>
<span className="text-xs text-muted-foreground">
{loadingStatus
? "加载中..."
: `${selected.size} / ${entries.length} 个文件`}
? t("loading")
: t("selectionCount", {
selected: selected.size,
total: entries.length,
})}
</span>
</div>
@@ -798,7 +840,7 @@ export function CommitWorkspace({
<ScrollArea className="h-full">
{entries.length === 0 && !loadingStatus ? (
<div className="px-3 py-4 text-center text-sm text-muted-foreground">
{t("emptyFiles")}
</div>
) : (
<div className="space-y-3 p-2">
@@ -816,15 +858,19 @@ export function CommitWorkspace({
)}
aria-label={
trackedAllSelected
? "取消选择已跟踪改动"
: "选择已跟踪改动"
? t("aria.unselectTracked")
: t("aria.selectTracked")
}
>
{trackedAllSelected && (
<Check className="h-2.5 w-2.5" />
)}
</button>
<span> ({trackedEntries.length})</span>
<span>
{t("trackedChanges", {
count: trackedEntries.length,
})}
</span>
</div>
<FileTree
className="rounded-none border-0 bg-transparent font-sans text-sm [&>div]:p-1"
@@ -854,8 +900,8 @@ export function CommitWorkspace({
)}
aria-label={
untrackedAllSelected
? "取消选择未跟踪文件"
: "选择未跟踪文件"
? t("aria.unselectUntracked")
: t("aria.selectUntracked")
}
>
{untrackedAllSelected && (
@@ -872,7 +918,11 @@ export function CommitWorkspace({
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
)}
<span> ({untrackedEntries.length})</span>
<span>
{t("untrackedFiles", {
count: untrackedEntries.length,
})}
</span>
</button>
</div>
{untrackedOpen && (
@@ -896,17 +946,19 @@ export function CommitWorkspace({
</div>
<div className="border-t p-3">
<div className="mb-2 text-xs text-muted-foreground"></div>
<div className="mb-2 text-xs text-muted-foreground">
{t("commitMessage")}
</div>
<Textarea
key={messageInputKey}
placeholder="输入提交信息..."
placeholder={t("commitMessagePlaceholder")}
defaultValue=""
onChange={handleMessageChange}
className="min-h-[90px] resize-y"
/>
<div className="mt-3 flex items-center justify-end gap-2">
<Button variant="outline" onClick={onCancel}>
{tCommon("cancel")}
</Button>
<Button
disabled={committing || !hasMessage || selected.size === 0}
@@ -915,7 +967,7 @@ export function CommitWorkspace({
{committing && (
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
)}
({selected.size})
{t("commitButton", { count: selected.size })}
</Button>
</div>
</div>
@@ -932,24 +984,24 @@ export function CommitWorkspace({
{!diffFile ? (
<>
<div className="flex h-9 items-center gap-3 border-b bg-muted/50 px-3 text-xs text-muted-foreground">
<span className="font-medium">HEAD</span>
<span className="font-medium">{t("head")}</span>
<span className="text-muted-foreground/60"></span>
<span className="font-medium">Working Tree</span>
<span className="font-medium">{t("workingTree")}</span>
</div>
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
{t("clickFileToDiff")}
</div>
</>
) : loadingDiff ? (
<>
<div className="flex h-9 items-center gap-3 border-b bg-muted/50 px-3 text-xs text-muted-foreground">
<span className="font-medium">HEAD</span>
<span className="font-medium">{t("head")}</span>
<span className="text-muted-foreground/60"></span>
<span className="font-medium">Working Tree</span>
<span className="font-medium">{t("workingTree")}</span>
</div>
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
{t("loadingDiff")}
</div>
</>
) : (
@@ -957,8 +1009,8 @@ export function CommitWorkspace({
key={diffFile}
original={diffOriginal}
modified={diffModified}
originalLabel="HEAD"
modifiedLabel="Working Tree"
originalLabel={t("head")}
modifiedLabel={t("workingTree")}
language={diffLanguage}
className="h-full [&>div:first-child]:h-9 [&>div:first-child]:py-0"
/>
@@ -981,14 +1033,14 @@ export function CommitWorkspace({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction
variant={
confirm.variant === "destructive" ? "destructive" : "default"
}
onClick={executeConfirmAction}
>
{tCommon("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>