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

@@ -12,6 +12,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { revealItemInDir } from "@tauri-apps/plugin-opener"
import ignore from "ignore"
import { Check, ChevronRight } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { useFolderContext } from "@/contexts/folder-context"
import { useAuxPanelContext } from "@/contexts/aux-panel-context"
@@ -107,14 +108,6 @@ function baseName(path: string): string {
const FILE_TREE_ROOT_PATH = "__workspace_root__"
const GITIGNORE_MUTED_CLASS = "text-muted-foreground/55"
function getSystemExplorerLabel(): string {
if (typeof navigator === "undefined") return "在文件管理器打开"
const platform = `${navigator.platform} ${navigator.userAgent}`.toLowerCase()
if (platform.includes("mac")) return "在访达打开"
if (platform.includes("win")) return "在资源管理器打开"
return "在文件管理器打开"
}
interface FileActionTarget {
kind: "file" | "dir"
path: string
@@ -447,14 +440,26 @@ function RenderNode({
onRequestDelete,
onRefresh,
}: RenderNodeProps) {
const t = useTranslations("Folder.fileTreeTab")
const tCommon = useTranslations("Folder.common")
const isGitignoreIgnored =
ancestorGitignoreIgnored || gitignoreIgnoredPaths.has(node.path)
const systemExplorerLabel =
typeof navigator === "undefined"
? t("openInFileManager")
: (() => {
const platform =
`${navigator.platform} ${navigator.userAgent}`.toLowerCase()
if (platform.includes("mac")) return t("openInFinder")
if (platform.includes("win")) return t("openInExplorer")
return t("openInFileManager")
})()
if (node.kind === "file") {
const gitStatusCode = gitStatusByPath.get(node.path)
const absolutePath = joinFsPath(workspacePath, node.path)
const dirPath = parentDir(absolutePath)
const systemExplorerLabel = getSystemExplorerLabel()
const isGitMenuDisabled = !gitEnabled || isGitignoreIgnored
const handleAttachToSession = () => {
@@ -470,7 +475,7 @@ function RenderNode({
await revealItemInDir(absolutePath)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("打开目录失败", { description: message })
toast.error(t("toasts.openDirectoryFailed"), { description: message })
}
}
@@ -489,17 +494,17 @@ function RenderNode({
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => onOpenFilePreview(node.path)}>
{tCommon("openFile")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => void handleAttachToSession()}
disabled={!activeSessionTabId}
>
{t("attachToCurrentSession")}
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger disabled={isGitMenuDisabled}>
Git
{t("git")}
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
@@ -509,35 +514,37 @@ function RenderNode({
classifyGitFileState(gitStatusCode ?? "") !== "untracked"
}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onOpenFileDiff(node.path)}
disabled={isGitMenuDisabled}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onRequestCompareWithBranch(node)}
disabled={isGitMenuDisabled}
>
...
{t("compareWithBranch")}
</ContextMenuItem>
<ContextMenuItem
variant="destructive"
onSelect={() => onRequestRollback(node)}
disabled={isGitMenuDisabled}
>
{t("actions.rollback")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onSelect={() => onRequestRename(node)}>
{tCommon("rename")}
</ContextMenuItem>
<ContextMenuItem onSelect={onRefresh}>
{t("reloadFromDisk")}
</ContextMenuItem>
<ContextMenuItem onSelect={onRefresh}></ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger></ContextMenuSubTrigger>
<ContextMenuSubTrigger>{t("openIn")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => void handleOpenInSystemExplorer()}
@@ -547,7 +554,7 @@ function RenderNode({
<ContextMenuItem
onSelect={() => void onOpenDirInTerminal(dirPath, node.name)}
>
{t("openInTerminal")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
@@ -555,7 +562,7 @@ function RenderNode({
onSelect={() => onRequestDelete(node)}
variant="destructive"
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -563,7 +570,6 @@ function RenderNode({
}
const absolutePath = joinFsPath(workspacePath, node.path)
const systemExplorerLabel = getSystemExplorerLabel()
const dirHasChanges = !isGitignoreIgnored && gitChangedDirPaths.has(node.path)
const isGitMenuDisabled = !gitEnabled || isGitignoreIgnored
const shouldRenderChildren = expandedPaths.has(node.path)
@@ -573,7 +579,7 @@ function RenderNode({
await revealItemInDir(absolutePath)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("打开目录失败", { description: message })
toast.error(t("toasts.openDirectoryFailed"), { description: message })
}
}
@@ -623,41 +629,41 @@ function RenderNode({
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger disabled={isGitMenuDisabled}>
Git
{t("git")}
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => onRequestAddToVcs(node)}
disabled={isGitMenuDisabled}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onOpenDirDiff(node.path)}
disabled={isGitMenuDisabled}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onRequestCompareWithBranch(node)}
disabled={isGitMenuDisabled}
>
...
{t("compareWithBranch")}
</ContextMenuItem>
<ContextMenuItem
variant="destructive"
onSelect={() => onRequestRollback(node)}
disabled={isGitMenuDisabled}
>
{t("actions.rollback")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onSelect={() => onRequestRename(node)}>
{tCommon("rename")}
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger></ContextMenuSubTrigger>
<ContextMenuSubTrigger>{t("openIn")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => void handleOpenDirInSystemExplorer()}
@@ -667,16 +673,18 @@ function RenderNode({
<ContextMenuItem
onSelect={() => void onOpenDirInTerminal(absolutePath, node.name)}
>
{t("openInTerminal")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onSelect={onRefresh}></ContextMenuItem>
<ContextMenuItem onSelect={onRefresh}>
{t("reloadFromDisk")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onRequestDelete(node)}
variant="destructive"
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -684,6 +692,8 @@ function RenderNode({
}
export function FileTreeTab() {
const t = useTranslations("Folder.fileTreeTab")
const tCommon = useTranslations("Folder.common")
const { activeTab } = useAuxPanelContext()
const { folder } = useFolderContext()
const { tabs, activeTabId } = useTabContext()
@@ -1095,13 +1105,13 @@ export function FileTreeTab() {
const handleOpenDirInTerminal = useCallback(
async (dirPath: string, fileName: string) => {
const terminalTitle = `Terminal · ${baseName(fileName)}`
const terminalTitle = t("terminalTitle", { name: baseName(fileName) })
const terminalId = await createTerminalInDirectory(dirPath, terminalTitle)
if (!terminalId) {
toast.error("无法打开内置终端")
toast.error(t("toasts.openBuiltinTerminalFailed"))
}
},
[createTerminalInDirectory]
[createTerminalInDirectory, t]
)
const handleRequestRename = useCallback((target: FileActionTarget) => {
@@ -1146,8 +1156,8 @@ export function FileTreeTab() {
resetDirectoryGitActionDialog()
toast.info(
action === "add"
? "该目录下没有可添加到VCS的变更文件"
: "该目录下没有可回滚的变更文件"
? t("toasts.noAddableFilesInDir")
: t("toasts.noRollbackFilesInDir")
)
return
}
@@ -1168,7 +1178,7 @@ export function FileTreeTab() {
setDirectoryGitLoading(false)
}
},
[folder?.path, resetDirectoryGitActionDialog]
[folder?.path, resetDirectoryGitActionDialog, t]
)
const handleRequestRollback = useCallback(
@@ -1191,14 +1201,14 @@ export function FileTreeTab() {
if (!folder?.path) return
try {
await gitAddFiles(folder.path, [target.path])
toast.success(`已添加 ${target.name} 到VCS`)
toast.success(t("toasts.addedToVcs", { name: target.name }))
await fetchTree()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("添加到VCS失败", { description: message })
toast.error(t("toasts.addToVcsFailed"), { description: message })
}
},
[fetchTree, folder?.path, openDirectoryGitActionDialog]
[fetchTree, folder?.path, openDirectoryGitActionDialog, t]
)
const loadCompareBranches = useCallback(async () => {
@@ -1222,7 +1232,7 @@ export function FileTreeTab() {
branchesResult.reason instanceof Error
? branchesResult.reason.message
: String(branchesResult.reason)
toast.error("加载分支失败", { description: message })
toast.error(t("toasts.loadBranchesFailed"), { description: message })
}
if (currentBranchResult.status === "fulfilled") {
@@ -1234,11 +1244,11 @@ export function FileTreeTab() {
setCompareBranchList({ local: [], remote: [], worktree_branches: [] })
setCompareCurrentBranch(null)
const message = error instanceof Error ? error.message : String(error)
toast.error("加载分支失败", { description: message })
toast.error(t("toasts.loadBranchesFailed"), { description: message })
} finally {
setCompareBranchLoading(false)
}
}, [folder?.path])
}, [folder?.path, t])
const handleRequestCompareWithBranch = useCallback(
(target: FileActionTarget) => {
@@ -1433,7 +1443,10 @@ export function FileTreeTab() {
? "flex h-4 w-4 shrink-0 items-center justify-center rounded border border-primary bg-primary text-primary-foreground transition-colors"
: "flex h-4 w-4 shrink-0 items-center justify-center rounded border border-input transition-colors"
}
aria-label={`${selected ? "取消选择" : "选择"} ${node.path}`}
aria-label={t("aria.selectPath", {
action: selected ? t("actions.unselect") : t("actions.select"),
path: node.path,
})}
disabled={directoryGitSubmitting}
>
{selected && <Check className="h-3 w-3" />}
@@ -1461,6 +1474,7 @@ export function FileTreeTab() {
directoryGitSelectedPaths,
directoryGitSubmitting,
handleToggleDirectoryGitFile,
t,
]
)
@@ -1480,11 +1494,11 @@ export function FileTreeTab() {
await fetchTree()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("重命名失败", { description: message })
toast.error(t("toasts.renameFailed"), { description: message })
} finally {
setRenaming(false)
}
}, [fetchTree, folder?.path, renameTarget, renameValue])
}, [fetchTree, folder?.path, renameTarget, renameValue, t])
const handleDeleteConfirm = useCallback(async () => {
if (!folder?.path || !deleteTarget) return
@@ -1495,27 +1509,27 @@ export function FileTreeTab() {
await fetchTree()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("删除失败", { description: message })
toast.error(t("toasts.deleteFailed"), { description: message })
} finally {
setDeleting(false)
}
}, [deleteTarget, fetchTree, folder?.path])
}, [deleteTarget, fetchTree, folder?.path, t])
const handleRollbackConfirm = useCallback(async () => {
if (!folder?.path || !rollbackTarget) return
setRollingBack(true)
try {
await gitRollbackFile(folder.path, rollbackTarget.path)
toast.success(`已回滚 ${rollbackTarget.name}`)
toast.success(t("toasts.rolledBack", { name: rollbackTarget.name }))
setRollbackTarget(null)
await fetchTree()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("回滚失败", { description: message })
toast.error(t("toasts.rollbackFailed"), { description: message })
} finally {
setRollingBack(false)
}
}, [fetchTree, folder?.path, rollbackTarget])
}, [fetchTree, folder?.path, rollbackTarget, t])
const handleDirectoryGitActionConfirm = useCallback(async () => {
if (!folder?.path || !directoryGitActionType) return
@@ -1528,12 +1542,20 @@ export function FileTreeTab() {
try {
if (directoryGitActionType === "add") {
await gitAddFiles(folder.path, selectedPaths)
toast.success(`已添加 ${selectedPaths.length} 个文件到VCS`)
toast.success(
t("toasts.addedFilesToVcs", {
count: selectedPaths.length,
})
)
} else {
for (const filePath of selectedPaths) {
await gitRollbackFile(folder.path, filePath)
}
toast.success(`已回滚 ${selectedPaths.length} 个文件`)
toast.success(
t("toasts.rolledBackFiles", {
count: selectedPaths.length,
})
)
}
resetDirectoryGitActionDialog()
@@ -1542,7 +1564,9 @@ export function FileTreeTab() {
const message = error instanceof Error ? error.message : String(error)
setDirectoryGitError(message)
toast.error(
directoryGitActionType === "add" ? "添加到VCS失败" : "回滚失败",
directoryGitActionType === "add"
? t("toasts.addToVcsFailed")
: t("toasts.rollbackFailed"),
{
description: message,
}
@@ -1556,6 +1580,7 @@ export function FileTreeTab() {
fetchTree,
folder?.path,
resetDirectoryGitActionDialog,
t,
])
const handleCompareBranchClick = useCallback(
@@ -1633,23 +1658,23 @@ export function FileTreeTab() {
externalConflictPrompt.path,
unsavedContent
)
toast.success("已另存为副本", {
toast.success(t("toasts.savedAsCopy"), {
description: result.path,
})
setExternalConflictPrompt(null)
void fetchTree({ silent: true })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("另存为副本失败", { description: message })
toast.error(t("toasts.saveCopyFailed"), { description: message })
} finally {
setSavingExternalConflictCopy(false)
}
}, [externalConflictPrompt, fetchTree, folder?.path])
}, [externalConflictPrompt, fetchTree, folder?.path, t])
const rootNodeName = useMemo(() => {
if (!folder?.path) return "Workspace"
if (!folder?.path) return t("workspace")
return baseName(folder.path)
}, [folder?.path])
}, [folder?.path, t])
useEffect(() => {
if (!isFileTreeTabActive) return
@@ -1807,7 +1832,7 @@ export function FileTreeTab() {
await startFileTreeWatch(rootPath)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("文件监听启动失败", { description: message })
toast.error(t("toasts.watchStartFailed"), { description: message })
}
try {
@@ -1915,7 +1940,7 @@ export function FileTreeTab() {
}
void stopFileTreeWatch(rootPath)
}
}, [fetchTree, folder?.path, openFilePreview])
}, [fetchTree, folder?.path, openFilePreview, t])
if (loading && nodes.length === 0) {
return (
@@ -1941,7 +1966,7 @@ export function FileTreeTab() {
void fetchTree()
}}
>
Retry
{t("retry")}
</Button>
</div>
)
@@ -2009,7 +2034,7 @@ export function FileTreeTab() {
void fetchTree()
}}
>
{t("reloadFromDisk")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -2025,10 +2050,12 @@ export function FileTreeTab() {
<DialogContent>
<DialogHeader>
<DialogTitle>
{renameTarget?.kind === "dir" ? "重命名目录" : "重命名文件"}
{renameTarget?.kind === "dir"
? t("renameDialog.renameDirectory")
: t("renameDialog.renameFile")}
</DialogTitle>
<DialogDescription>
{t("renameDialog.description")}
</DialogDescription>
</DialogHeader>
<form
@@ -2045,8 +2072,8 @@ export function FileTreeTab() {
disabled={renaming}
placeholder={
renameTarget?.kind === "dir"
? "new-folder-name"
: "new-file-name.ext"
? t("renameDialog.placeholderDirectory")
: t("renameDialog.placeholderFile")
}
/>
<DialogFooter>
@@ -2059,10 +2086,10 @@ export function FileTreeTab() {
setRenameValue("")
}}
>
{tCommon("cancel")}
</Button>
<Button type="submit" disabled={renaming}>
{tCommon("confirm")}
</Button>
</DialogFooter>
</form>
@@ -2079,19 +2106,29 @@ export function FileTreeTab() {
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{directoryGitActionType === "add" ? "添加到VCS" : "回滚"}
{directoryGitActionType === "add"
? t("actions.addToVcs")
: t("actions.rollback")}
</DialogTitle>
<DialogDescription>
{directoryGitActionTarget
? `选择目录 ${directoryGitActionTarget.path} 下要${directoryGitActionType === "add" ? "添加到VCS" : "回滚"}的文件。`
: "选择要操作的文件。"}
? directoryGitActionType === "add"
? t("directoryDialog.descriptionAdd", {
path: directoryGitActionTarget.path,
})
: t("directoryDialog.descriptionRollback", {
path: directoryGitActionTarget.path,
})
: t("directoryDialog.descriptionFallback")}
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="flex items-center justify-between gap-2 text-xs">
<span className="text-muted-foreground">
{directoryGitSelectedPaths.size} /{" "}
{directoryGitAllFilePaths.length}
{t("directoryDialog.selectionCount", {
selected: directoryGitSelectedPaths.size,
total: directoryGitAllFilePaths.length,
})}
</span>
<Button
type="button"
@@ -2100,13 +2137,15 @@ export function FileTreeTab() {
disabled={directoryGitLoading || directoryGitSubmitting}
onClick={handleToggleDirectoryGitSelectAll}
>
{directoryGitAllSelected ? "取消全选" : "全选"}
{directoryGitAllSelected
? t("directoryDialog.unselectAll")
: t("directoryDialog.selectAll")}
</Button>
</div>
<div className="max-h-80 overflow-auto rounded-md border">
{directoryGitLoading ? (
<div className="py-8 text-center text-xs text-muted-foreground">
...
{t("directoryDialog.loadingCandidates")}
</div>
) : directoryGitError ? (
<div className="p-3 text-xs text-destructive">
@@ -2132,7 +2171,7 @@ export function FileTreeTab() {
</FileTree>
) : (
<div className="py-8 text-center text-xs text-muted-foreground">
{t("directoryDialog.noOperableFiles")}
</div>
)}
</div>
@@ -2143,7 +2182,7 @@ export function FileTreeTab() {
disabled={directoryGitSubmitting}
onClick={resetDirectoryGitActionDialog}
>
{tCommon("cancel")}
</Button>
<Button
type="button"
@@ -2161,7 +2200,9 @@ export function FileTreeTab() {
void handleDirectoryGitActionConfirm()
}}
>
{directoryGitActionType === "add" ? "添加到VCS" : "回滚"}
{directoryGitActionType === "add"
? t("actions.addToVcs")
: t("actions.rollback")}
</Button>
</DialogFooter>
</div>
@@ -2179,29 +2220,35 @@ export function FileTreeTab() {
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("compareDialog.title")}</DialogTitle>
<DialogDescription>
{compareTarget
? `选择分支并与${compareTarget.kind === "dir" ? "目录" : "文件"} ${compareTarget.path} 对比`
: "选择要比较的分支。"}
? t("compareDialog.descriptionWithTarget", {
kind:
compareTarget.kind === "dir"
? t("compareDialog.kindDirectory")
: t("compareDialog.kindFile"),
path: compareTarget.path,
})
: t("compareDialog.descriptionFallback")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Input
value={compareBranchFilter}
onChange={(event) => setCompareBranchFilter(event.target.value)}
placeholder="过滤分支,例如 main / origin/main"
placeholder={t("compareDialog.filterPlaceholder")}
autoFocus
disabled={comparing}
/>
<div className="text-xs text-muted-foreground">
{t("compareDialog.singleClickHint")}
</div>
<div className="space-y-2">
<div className="max-h-56 overflow-y-auto rounded-xl border p-2 space-y-3">
{compareBranchLoading ? (
<div className="py-6 text-center text-xs text-muted-foreground">
...
{t("compareDialog.loadingBranches")}
</div>
) : (
<>
@@ -2211,7 +2258,9 @@ export function FileTreeTab() {
>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
({filteredCompareRecentBranches.length})
{t("compareDialog.recentBranches", {
count: filteredCompareRecentBranches.length,
})}
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1">
{filteredCompareRecentBranches.length > 0 ? (
@@ -2232,7 +2281,7 @@ export function FileTreeTab() {
))
) : (
<div className="px-2 text-xs text-muted-foreground">
{t("compareDialog.noCurrentBranch")}
</div>
)}
</CollapsibleContent>
@@ -2243,7 +2292,9 @@ export function FileTreeTab() {
>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
({filteredCompareBranches.local.length})
{t("compareDialog.localBranches", {
count: filteredCompareBranches.local.length,
})}
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1">
{filteredCompareBranches.local.length > 0 ? (
@@ -2264,7 +2315,7 @@ export function FileTreeTab() {
))
) : (
<div className="px-2 text-xs text-muted-foreground">
{t("compareDialog.noMatchingBranches")}
</div>
)}
</CollapsibleContent>
@@ -2275,7 +2326,9 @@ export function FileTreeTab() {
>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
({filteredCompareBranches.remote.length})
{t("compareDialog.remoteBranches", {
count: filteredCompareBranches.remote.length,
})}
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1">
{filteredCompareBranches.remote.length > 0 ? (
@@ -2296,7 +2349,7 @@ export function FileTreeTab() {
))
) : (
<div className="px-2 text-xs text-muted-foreground">
{t("compareDialog.noMatchingBranches")}
</div>
)}
</CollapsibleContent>
@@ -2316,7 +2369,7 @@ export function FileTreeTab() {
setCompareCurrentBranch(null)
}}
>
{tCommon("cancel")}
</Button>
</DialogFooter>
</div>
@@ -2332,11 +2385,13 @@ export function FileTreeTab() {
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("externalConflictDialog.title")}</DialogTitle>
<DialogDescription>
{externalConflictPrompt
? `文件 ${externalConflictPrompt.path} 在磁盘已发生变化,当前编辑内容尚未保存。`
: "当前文件在磁盘已发生变化,当前编辑内容尚未保存。"}
? t("externalConflictDialog.descriptionWithPath", {
path: externalConflictPrompt.path,
})
: t("externalConflictDialog.descriptionFallback")}
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -2346,7 +2401,7 @@ export function FileTreeTab() {
disabled={savingExternalConflictCopy}
onClick={handleCompareExternalConflict}
>
{t("externalConflictDialog.compare")}
</Button>
<Button
type="button"
@@ -2356,7 +2411,9 @@ export function FileTreeTab() {
void handleSaveExternalConflictCopy()
}}
>
{savingExternalConflictCopy ? "另存中..." : "另存为副本"}
{savingExternalConflictCopy
? t("externalConflictDialog.savingCopy")
: t("externalConflictDialog.saveAsCopy")}
</Button>
<Button
type="button"
@@ -2364,7 +2421,7 @@ export function FileTreeTab() {
disabled={savingExternalConflictCopy}
onClick={handleReloadExternalConflict}
>
{t("externalConflictDialog.reload")}
</Button>
</DialogFooter>
</DialogContent>
@@ -2379,15 +2436,23 @@ export function FileTreeTab() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t("deleteConfirm.title")}</AlertDialogTitle>
<AlertDialogDescription>
{deleteTarget
? `确定删除${deleteTarget.kind === "dir" ? "目录" : "文件"} "${deleteTarget.name}" 吗?此操作不可撤销。`
: "此操作不可撤销。"}
? t("deleteConfirm.descriptionWithTarget", {
kind:
deleteTarget.kind === "dir"
? t("deleteConfirm.kindDirectory")
: t("deleteConfirm.kindFile"),
name: deleteTarget.name,
})
: t("deleteConfirm.descriptionFallback")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}></AlertDialogCancel>
<AlertDialogCancel disabled={deleting}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={deleting}
@@ -2395,7 +2460,7 @@ export function FileTreeTab() {
void handleDeleteConfirm()
}}
>
{tCommon("delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -2410,15 +2475,19 @@ export function FileTreeTab() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t("rollbackConfirm.title")}</AlertDialogTitle>
<AlertDialogDescription>
{rollbackTarget
? `确定回滚文件 "${rollbackTarget.name}" 的本地修改吗?`
: "确定回滚该文件的本地修改吗?"}
? t("rollbackConfirm.descriptionWithTarget", {
name: rollbackTarget.name,
})
: t("rollbackConfirm.descriptionFallback")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={rollingBack}></AlertDialogCancel>
<AlertDialogCancel disabled={rollingBack}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={rollingBack}
@@ -2426,7 +2495,7 @@ export function FileTreeTab() {
void handleRollbackConfirm()
}}
>
{t("actions.rollback")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -10,6 +10,7 @@ import {
} from "react"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { ChevronsDownUp, ChevronsUpDown } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import {
CommitFileAdditions,
@@ -423,6 +424,8 @@ function toWorkingTreeChanges(
}
export function GitChangesTab() {
const t = useTranslations("Folder.gitChangesTab")
const tCommon = useTranslations("Folder.common")
const { folder } = useFolderContext()
const { activeTab } = useAuxPanelContext()
const { openFilePreview, openWorkingTreeDiff } = useWorkspaceContext()
@@ -466,8 +469,8 @@ export function GitChangesTab() {
const folderName = useMemo(() => {
const path = folder?.path ?? ""
const parts = path.split(/[\\/]/).filter(Boolean)
return (parts[parts.length - 1] ?? path) || "workspace"
}, [folder?.path])
return (parts[parts.length - 1] ?? path) || t("workspace")
}, [folder?.path, t])
const trackedChanges = useMemo(
() => changes.filter((change) => !isUntrackedStatus(change.status)),
@@ -712,8 +715,8 @@ export function GitChangesTab() {
resetDirectoryGitActionDialog()
toast.info(
action === "add"
? "该目录下没有可添加到VCS的变更文件"
: "该目录下没有可回滚的变更文件"
? t("toasts.noAddableFilesInDir")
: t("toasts.noRollbackFilesInDir")
)
return
}
@@ -729,7 +732,7 @@ export function GitChangesTab() {
setDirectoryGitLoading(false)
}
},
[folder?.path, resetDirectoryGitActionDialog]
[folder?.path, resetDirectoryGitActionDialog, t]
)
const handleRequestRollback = useCallback(
@@ -753,14 +756,14 @@ export function GitChangesTab() {
if (!folder?.path) return
try {
await gitAddFiles(folder.path, [target.path])
toast.success(`已添加 ${target.name} 到VCS`)
toast.success(t("toasts.addedToVcs", { name: target.name }))
await fetchChanges({ inline: true })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("添加到VCS失败", { description: message })
toast.error(t("toasts.addToVcsFailed"), { description: message })
}
},
[fetchChanges, folder?.path, openDirectoryGitActionDialog]
[fetchChanges, folder?.path, openDirectoryGitActionDialog, t]
)
const handleRollbackConfirm = useCallback(async () => {
@@ -769,16 +772,16 @@ export function GitChangesTab() {
setRollingBack(true)
try {
await gitRollbackFile(folder.path, rollbackTarget.path)
toast.success(`已回滚 ${rollbackTarget.name}`)
toast.success(t("toasts.rolledBack", { name: rollbackTarget.name }))
setRollbackTarget(null)
await fetchChanges({ inline: true })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error("回滚失败", { description: message })
toast.error(t("toasts.rollbackFailed"), { description: message })
} finally {
setRollingBack(false)
}
}, [fetchChanges, folder?.path, rollbackTarget])
}, [fetchChanges, folder?.path, rollbackTarget, t])
const directoryGitAllFilePaths = useMemo(
() => directoryGitCandidates.map((entry) => entry.path),
@@ -830,12 +833,20 @@ export function GitChangesTab() {
try {
if (directoryGitActionType === "add") {
await gitAddFiles(folder.path, selectedPaths)
toast.success(`已添加 ${selectedPaths.length} 个文件到VCS`)
toast.success(
t("toasts.addedFilesToVcs", {
count: selectedPaths.length,
})
)
} else {
for (const filePath of selectedPaths) {
await gitRollbackFile(folder.path, filePath)
}
toast.success(`已回滚 ${selectedPaths.length} 个文件`)
toast.success(
t("toasts.rolledBackFiles", {
count: selectedPaths.length,
})
)
}
resetDirectoryGitActionDialog()
@@ -844,7 +855,9 @@ export function GitChangesTab() {
const message = error instanceof Error ? error.message : String(error)
setDirectoryGitError(message)
toast.error(
directoryGitActionType === "add" ? "添加到VCS失败" : "回滚失败",
directoryGitActionType === "add"
? t("toasts.addToVcsFailed")
: t("toasts.rollbackFailed"),
{
description: message,
}
@@ -858,6 +871,7 @@ export function GitChangesTab() {
fetchChanges,
folder?.path,
resetDirectoryGitActionDialog,
t,
])
useEffect(() => {
@@ -893,7 +907,7 @@ export function GitChangesTab() {
void openWorkingTreeDiff(node.path, { mode: "overview" })
}}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -901,14 +915,14 @@ export function GitChangesTab() {
}}
variant="destructive"
>
{t("actions.rollback")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void handleAddToVcs(target)
}}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -959,14 +973,14 @@ export function GitChangesTab() {
void openFilePreview(file.path)
}}
>
{tCommon("openFile")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void openWorkingTreeDiff(file.path)
}}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -974,14 +988,14 @@ export function GitChangesTab() {
}}
variant="destructive"
>
{t("actions.rollback")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void handleAddToVcs(target)
}}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -992,6 +1006,8 @@ export function GitChangesTab() {
handleRequestRollback,
openFilePreview,
openWorkingTreeDiff,
t,
tCommon,
]
)
@@ -1023,7 +1039,7 @@ export function GitChangesTab() {
void openWorkingTreeDiff(node.path, { mode: "overview" })
}}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -1031,14 +1047,14 @@ export function GitChangesTab() {
}}
variant="destructive"
>
{t("actions.rollback")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void handleAddToVcs(target)
}}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -1079,14 +1095,14 @@ export function GitChangesTab() {
void openFilePreview(file.path)
}}
>
{tCommon("openFile")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void openWorkingTreeDiff(file.path)
}}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -1094,14 +1110,14 @@ export function GitChangesTab() {
}}
variant="destructive"
>
{t("actions.rollback")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void handleAddToVcs(target)
}}
>
VCS
{t("actions.addToVcs")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -1112,6 +1128,8 @@ export function GitChangesTab() {
handleRequestRollback,
openFilePreview,
openWorkingTreeDiff,
t,
tCommon,
]
)
@@ -1139,14 +1157,16 @@ export function GitChangesTab() {
<div className="h-full min-h-0 overflow-y-auto">
{trackedChanges.length === 0 && untrackedChanges.length === 0 ? (
<div className="px-2 py-2 text-xs text-muted-foreground">
{t("noChanges")}
</div>
) : (
<div className="space-y-2 pb-2">
{trackedChanges.length > 0 && (
<section className="space-y-1">
<div className="flex items-center justify-between px-2 py-1 text-[11px] text-muted-foreground">
<span> ({trackedChanges.length})</span>
<span>
{t("trackedChanges", { count: trackedChanges.length })}
</span>
<Button
variant="ghost"
size="icon"
@@ -1154,10 +1174,14 @@ export function GitChangesTab() {
onClick={toggleTrackedExpanded}
disabled={!trackedCanExpand && !trackedCanCollapse}
title={
trackedCanExpand ? "展开已跟踪改动" : "折叠已跟踪改动"
trackedCanExpand
? t("expandTracked")
: t("collapseTracked")
}
aria-label={
trackedCanExpand ? "展开已跟踪改动" : "折叠已跟踪改动"
trackedCanExpand
? t("expandTracked")
: t("collapseTracked")
}
>
{trackedCanExpand ? (
@@ -1188,7 +1212,9 @@ export function GitChangesTab() {
{untrackedChanges.length > 0 && (
<section className="space-y-1">
<div className="flex items-center justify-between px-2 py-1 text-[11px] text-muted-foreground">
<span> ({untrackedChanges.length})</span>
<span>
{t("untrackedFiles", { count: untrackedChanges.length })}
</span>
<Button
variant="ghost"
size="icon"
@@ -1196,10 +1222,14 @@ export function GitChangesTab() {
onClick={toggleUntrackedExpanded}
disabled={!untrackedCanExpand && !untrackedCanCollapse}
title={
untrackedCanExpand ? "展开未跟踪文件" : "折叠未跟踪文件"
untrackedCanExpand
? t("expandUntracked")
: t("collapseUntracked")
}
aria-label={
untrackedCanExpand ? "展开未跟踪文件" : "折叠未跟踪文件"
untrackedCanExpand
? t("expandUntracked")
: t("collapseUntracked")
}
>
{untrackedCanExpand ? (
@@ -1240,19 +1270,29 @@ export function GitChangesTab() {
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{directoryGitActionType === "add" ? "添加到VCS" : "回滚"}
{directoryGitActionType === "add"
? t("actions.addToVcs")
: t("actions.rollback")}
</DialogTitle>
<DialogDescription>
{directoryGitActionTarget
? `选择目录 ${directoryGitActionTarget.path} 下要${directoryGitActionType === "add" ? "添加到VCS" : "回滚"}的文件。`
: "选择要操作的文件。"}
? directoryGitActionType === "add"
? t("directoryDialog.descriptionAdd", {
path: directoryGitActionTarget.path,
})
: t("directoryDialog.descriptionRollback", {
path: directoryGitActionTarget.path,
})
: t("directoryDialog.descriptionFallback")}
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="flex items-center justify-between gap-2 text-xs">
<span className="text-muted-foreground">
{directoryGitSelectedPaths.size} /{" "}
{directoryGitAllFilePaths.length}
{t("directoryDialog.selectionCount", {
selected: directoryGitSelectedPaths.size,
total: directoryGitAllFilePaths.length,
})}
</span>
<Button
type="button"
@@ -1261,13 +1301,15 @@ export function GitChangesTab() {
disabled={directoryGitLoading || directoryGitSubmitting}
onClick={handleToggleDirectoryGitSelectAll}
>
{directoryGitAllSelected ? "取消全选" : "全选"}
{directoryGitAllSelected
? t("directoryDialog.unselectAll")
: t("directoryDialog.selectAll")}
</Button>
</div>
<div className="max-h-80 overflow-auto rounded-md border">
{directoryGitLoading ? (
<div className="py-8 text-center text-xs text-muted-foreground">
...
{t("directoryDialog.loadingCandidates")}
</div>
) : directoryGitError ? (
<div className="p-3 text-xs text-destructive">
@@ -1309,7 +1351,7 @@ export function GitChangesTab() {
</div>
) : (
<div className="py-8 text-center text-xs text-muted-foreground">
{t("directoryDialog.noOperableFiles")}
</div>
)}
</div>
@@ -1320,7 +1362,7 @@ export function GitChangesTab() {
disabled={directoryGitSubmitting}
onClick={resetDirectoryGitActionDialog}
>
{tCommon("cancel")}
</Button>
<Button
type="button"
@@ -1338,7 +1380,9 @@ export function GitChangesTab() {
void handleDirectoryGitActionConfirm()
}}
>
{directoryGitActionType === "add" ? "添加到VCS" : "回滚"}
{directoryGitActionType === "add"
? t("actions.addToVcs")
: t("actions.rollback")}
</Button>
</DialogFooter>
</div>
@@ -1354,15 +1398,23 @@ export function GitChangesTab() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t("rollbackConfirm.title")}</AlertDialogTitle>
<AlertDialogDescription>
{rollbackTarget
? `确定回滚${rollbackTarget.kind === "dir" ? "目录" : "文件"} "${rollbackTarget.name}" 的本地修改吗?`
: "确定回滚本地修改吗?"}
? t("rollbackConfirm.descriptionWithTarget", {
kind:
rollbackTarget.kind === "dir"
? t("rollbackConfirm.kindDirectory")
: t("rollbackConfirm.kindFile"),
name: rollbackTarget.name,
})
: t("rollbackConfirm.descriptionFallback")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={rollingBack}></AlertDialogCancel>
<AlertDialogCancel disabled={rollingBack}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={rollingBack}
@@ -1370,7 +1422,7 @@ export function GitChangesTab() {
void handleRollbackConfirm()
}}
>
{t("actions.rollback")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -8,6 +8,7 @@ import {
useMemo,
useState,
} from "react"
import { useTranslations } from "next-intl"
import {
ChevronsDownUp,
ChevronsUpDown,
@@ -83,7 +84,18 @@ import {
import type { GitBranchList, GitLogEntry, GitLogFileChange } from "@/lib/types"
import { toast } from "sonner"
function formatRelativeTime(dateStr: string): string {
function formatRelativeTime(
dateStr: string,
t: (
key:
| "time.monthsAgo"
| "time.daysAgo"
| "time.hoursAgo"
| "time.minsAgo"
| "time.justNow",
values?: { count: number }
) => string
): string {
const date = new Date(dateStr)
if (Number.isNaN(date.getTime())) return dateStr
@@ -95,13 +107,12 @@ function formatRelativeTime(dateStr: string): string {
if (diffDay > 30) {
const diffMonth = Math.floor(diffDay / 30)
return diffMonth === 1 ? "1 month ago" : `${diffMonth} months ago`
return t("time.monthsAgo", { count: diffMonth })
}
if (diffDay > 0) return diffDay === 1 ? "1 day ago" : `${diffDay} days ago`
if (diffHour > 0)
return diffHour === 1 ? "1 hour ago" : `${diffHour} hours ago`
if (diffMin > 0) return diffMin === 1 ? "1 min ago" : `${diffMin} mins ago`
return "just now"
if (diffDay > 0) return t("time.daysAgo", { count: diffDay })
if (diffHour > 0) return t("time.hoursAgo", { count: diffHour })
if (diffMin > 0) return t("time.minsAgo", { count: diffMin })
return t("time.justNow", { count: 0 })
}
function parseDate(dateStr: string): Date | null {
@@ -137,14 +148,21 @@ function mapFileStatus(
}
}
function getPushStatusMeta(pushed: boolean | null): {
function getPushStatusMeta(
pushed: boolean | null,
labels: {
pushed: string
notPushed: string
unknown: string
}
): {
label: string
icon: typeof CloudCheck
className: string
} {
if (pushed === true) {
return {
label: "Pushed to remote",
label: labels.pushed,
icon: CloudCheck,
className: "text-emerald-500",
}
@@ -152,14 +170,14 @@ function getPushStatusMeta(pushed: boolean | null): {
if (pushed === false) {
return {
label: "Not pushed to remote",
label: labels.notPushed,
icon: CloudOff,
className: "text-amber-500",
}
}
return {
label: "Push status unknown (no upstream configured)",
label: labels.unknown,
icon: CircleHelp,
className: "text-muted-foreground",
}
@@ -348,6 +366,8 @@ function CommitFilesTree({
) => void
onOpenFilePreview: (path: string) => void
}) {
const t = useTranslations("Folder.gitLogTab")
const tCommon = useTranslations("Folder.common")
const rootPath = "__commit_file_tree_root__"
const treeNodes = useMemo(() => buildCommitFileTree(files), [files])
const allDirectoryPaths = useMemo(() => {
@@ -431,14 +451,14 @@ function CommitFilesTree({
void onOpenCommitDiff(commitHash, file.path)
}}
>
{tCommon("viewDiff")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
void onOpenFilePreview(file.path)
}}
>
{tCommon("openFile")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -448,7 +468,7 @@ function CommitFilesTree({
return (
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] text-muted-foreground">Files</p>
<p className="text-[11px] text-muted-foreground">{t("filesTitle")}</p>
<div className="flex items-center gap-1">
<Button
variant="ghost"
@@ -456,8 +476,10 @@ function CommitFilesTree({
className="size-5"
onClick={toggleExpanded}
disabled={!canExpandAll && !canCollapseAll}
title={canExpandAll ? "展开全部文件" : "折叠全部文件"}
aria-label={canExpandAll ? "展开全部文件" : "折叠全部文件"}
title={canExpandAll ? t("expandAllFiles") : t("collapseAllFiles")}
aria-label={
canExpandAll ? t("expandAllFiles") : t("collapseAllFiles")
}
>
{canExpandAll ? (
<ChevronsUpDown className="size-3.5" />
@@ -503,6 +525,7 @@ function BranchSelector({
onRefresh: () => void
refreshing: boolean
}) {
const t = useTranslations("Folder.gitLogTab.branchSelector")
return (
<div className="flex items-center gap-1">
<Select value={selectedBranch ?? ""} onValueChange={onBranchChange}>
@@ -511,12 +534,12 @@ function BranchSelector({
className="cursor-pointer flex-1 w-full text-xs bg-input/30 hover:bg-input/50 aria-expanded:bg-muted"
>
<GitBranch className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<SelectValue placeholder="选择分支..." />
<SelectValue placeholder={t("selectBranchPlaceholder")} />
</SelectTrigger>
<SelectContent position="popper" sideOffset={4}>
{branchList.local.length > 0 && (
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectLabel>{t("localBranches")}</SelectLabel>
{branchList.local.map((branch) => (
<SelectItem
key={`local-${branch}`}
@@ -526,7 +549,7 @@ function BranchSelector({
{branch}
{branch === currentBranch && (
<span className="ml-auto text-[10px] text-muted-foreground">
{t("current")}
</span>
)}
</SelectItem>
@@ -537,7 +560,7 @@ function BranchSelector({
<>
{branchList.local.length > 0 && <SelectSeparator />}
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectLabel>{t("remoteBranches")}</SelectLabel>
{branchList.remote.map((branch) => (
<SelectItem
key={`remote-${branch}`}
@@ -558,8 +581,8 @@ function BranchSelector({
className="h-8 w-8 shrink-0 rounded-full"
onClick={onRefresh}
disabled={refreshing}
title="刷新提交记录"
aria-label="刷新提交记录"
title={t("refreshCommitHistory")}
aria-label={t("refreshCommitHistory")}
>
<RefreshCw className={`size-3.5 ${refreshing ? "animate-spin" : ""}`} />
</Button>
@@ -568,6 +591,8 @@ function BranchSelector({
}
export function GitLogTab() {
const t = useTranslations("Folder.gitLogTab")
const tCommon = useTranslations("Folder.common")
const { folder } = useFolderContext()
const { openCommitDiff, openFilePreview } = useWorkspaceContext()
const [entries, setEntries] = useState<GitLogEntry[]>([])
@@ -599,11 +624,19 @@ export function GitLogTab() {
const hasBranches =
branchList.local.length > 0 || branchList.remote.length > 0
const pushStatusLabels = useMemo(
() => ({
pushed: t("pushStatus.pushed"),
notPushed: t("pushStatus.notPushed"),
unknown: t("pushStatus.unknown"),
}),
[t]
)
const folderName = useMemo(() => {
const path = folder?.path ?? ""
const parts = path.split(/[\\/]/).filter(Boolean)
return (parts[parts.length - 1] ?? path) || "workspace"
}, [folder?.path])
return (parts[parts.length - 1] ?? path) || t("workspace")
}, [folder?.path, t])
const handleBranchChange = useCallback((branch: string) => {
setSelectedBranch(branch)
@@ -728,11 +761,14 @@ export function GitLogTab() {
setNewBranchTarget(null)
setNewBranchName("")
await refreshBranches(name)
toast.success("已创建并切换到新分支", {
description: `${name} (from ${newBranchTarget.shortHash})`,
toast.success(t("toasts.createdAndSwitchedNewBranch"), {
description: t("toasts.newBranchFromCommit", {
name,
shortHash: newBranchTarget.shortHash,
}),
})
} catch (error) {
toast.error("新建分支失败", {
toast.error(t("toasts.createBranchFailed"), {
description: error instanceof Error ? error.message : String(error),
})
} finally {
@@ -744,6 +780,7 @@ export function GitLogTab() {
newBranchName,
newBranchTarget,
refreshBranches,
t,
])
useEffect(() => {
@@ -804,7 +841,7 @@ export function GitLogTab() {
void fetchLog()
}}
>
Retry
{t("retry")}
</Button>
</div>
</div>
@@ -825,7 +862,7 @@ export function GitLogTab() {
/>
)}
<div className="pt-1 text-xs text-muted-foreground">
No commits found
{t("noCommitsFound")}
</div>
</div>
)
@@ -856,7 +893,10 @@ export function GitLogTab() {
{entries.map((entry) => {
const commitKey = entry.full_hash
const commitDate = parseDate(entry.date)
const pushStatus = getPushStatusMeta(entry.pushed)
const pushStatus = getPushStatusMeta(
entry.pushed,
pushStatusLabels
)
const PushStatusIcon = pushStatus.icon
const commitBranches = branchesByCommit[commitKey]
const isBranchLoading = !!branchesLoading[commitKey]
@@ -900,7 +940,7 @@ export function GitLogTab() {
className="shrink-0"
date={commitDate ?? new Date()}
>
{formatRelativeTime(entry.date)}
{formatRelativeTime(entry.date, t)}
</CommitTimestamp>
<CommitHash className="text-primary/70">
{entry.hash}
@@ -919,8 +959,10 @@ export function GitLogTab() {
entry.message
)
}}
title="查看差异"
aria-label={`查看提交 ${entry.hash} 的差异`}
title={tCommon("viewDiff")}
aria-label={t("viewCommitDiffAria", {
hash: entry.hash,
})}
>
<GitCompare size={14} />
</Button>
@@ -930,7 +972,7 @@ export function GitLogTab() {
<div className="space-y-3">
<div className="grid grid-cols-[4rem_minmax(0,1fr)] items-center gap-x-2 gap-y-1 text-xs">
<span className="text-muted-foreground">
Hash
{t("hash")}
</span>
<span className="group/hash flex items-center gap-1 min-w-0">
<code
@@ -940,14 +982,16 @@ export function GitLogTab() {
{entry.full_hash}
</code>
<CommitCopyButton
aria-label={`Copy full commit hash ${entry.full_hash}`}
aria-label={t("copyFullCommitHashAria", {
hash: entry.full_hash,
})}
className="size-5 shrink-0 opacity-0 transition-opacity group-hover/hash:opacity-100 group-focus-within/hash:opacity-100"
hash={entry.full_hash}
title="Copy hash"
title={t("copyHash")}
/>
</span>
<span className="text-muted-foreground">
Author
{t("author")}
</span>
<span className="min-w-0 flex items-center gap-1">
<span className="min-w-0 truncate">
@@ -974,10 +1018,10 @@ export function GitLogTab() {
{entry.files.length === 0 ? (
<div className="space-y-1">
<p className="text-[11px] text-muted-foreground">
Files
{t("filesTitle")}
</p>
<p className="text-xs text-muted-foreground">
No file change details available.
{t("noFileChangeDetails")}
</p>
</div>
) : (
@@ -991,11 +1035,11 @@ export function GitLogTab() {
)}
<div className="pt-3 space-y-1">
<p className="text-[11px] text-muted-foreground">
Branches
{t("branchesTitle")}
</p>
{isBranchLoading ? (
<p className="text-xs text-muted-foreground">
Loading branches...
{t("loadingBranches")}
</p>
) : branchError ? (
<p className="text-xs text-destructive">
@@ -1016,7 +1060,7 @@ export function GitLogTab() {
</div>
) : (
<p className="text-xs text-muted-foreground">
No containing branches found.
{t("noContainingBranches")}
</p>
)}
</div>
@@ -1032,7 +1076,7 @@ export function GitLogTab() {
}}
>
<GitBranchPlus className="h-3.5 w-3.5" />
...
{t("newBranch")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -1044,7 +1088,7 @@ export function GitLogTab() {
}}
>
<GitCompare className="h-3.5 w-3.5" />
{tCommon("viewDiff")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -1058,7 +1102,7 @@ export function GitLogTab() {
void fetchLog()
}}
>
{tCommon("refresh")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -1074,14 +1118,15 @@ export function GitLogTab() {
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("dialogs.newBranchTitle")}</DialogTitle>
<DialogDescription>
{newBranchTarget?.shortHash ?? "-"}{" "}
{t("dialogs.newBranchDescription", {
shortHash: newBranchTarget?.shortHash ?? "-",
})}
</DialogDescription>
</DialogHeader>
<Input
placeholder="分支名称"
placeholder={t("dialogs.branchNamePlaceholder")}
value={newBranchName}
onChange={(event) => setNewBranchName(event.target.value)}
onKeyDown={(event) => {
@@ -1105,7 +1150,7 @@ export function GitLogTab() {
setNewBranchName("")
}}
>
{tCommon("cancel")}
</Button>
<Button
disabled={!newBranchName.trim() || creatingBranch}
@@ -1113,7 +1158,7 @@ export function GitLogTab() {
void handleCreateBranchFromCommit()
}}
>
{tCommon("createAndSwitch")}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -57,6 +57,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -106,6 +107,8 @@ export function BranchDropdown({
parentBranch,
onBranchChange,
}: BranchDropdownProps) {
const t = useTranslations("Folder.branchDropdown")
const tCommon = useTranslations("Folder.common")
const { folder } = useFolderContext()
const folderPath = folder?.path ?? ""
const { addTask, updateTask, removeTask } = useTaskContext()
@@ -142,8 +145,10 @@ export function BranchDropdown({
"folder://git-commit-succeeded",
(event) => {
if (event.payload.folder_id !== folder.id) return
toast.success("提交代码完成", {
description: `已提交 ${event.payload.committed_files} 个文件`,
toast.success(t("toasts.commitCodeCompleted"), {
description: t("toasts.committedFiles", {
count: event.payload.committed_files,
}),
})
onBranchChange()
}
@@ -158,7 +163,7 @@ export function BranchDropdown({
return () => {
if (unlisten) unlisten()
}
}, [folder, onBranchChange])
}, [folder, onBranchChange, t])
async function runGitTask<T>(
label: string,
@@ -175,7 +180,7 @@ export function BranchDropdown({
updateTask(taskId, { status: "completed" })
onBranchChange()
toast.success(
`${label} 完成`,
t("toasts.taskCompleted", { label }),
successDescription
? {
description: successDescription,
@@ -184,8 +189,9 @@ export function BranchDropdown({
)
} catch (err) {
removeTask(taskId)
pushAlert("error", `${label}失败`, String(err))
toast.error(`${label} 失败`, { description: String(err) })
const errorTitle = t("toasts.taskFailed", { label })
pushAlert("error", errorTitle, String(err))
toast.error(errorTitle, { description: String(err) })
} finally {
setLoading(false)
}
@@ -220,7 +226,9 @@ export function BranchDropdown({
if (!name) return
setNewBranchOpen(false)
setNewBranchName("")
await runGitTask(`新建分支 ${name}`, () => gitNewBranch(folderPath, name))
await runGitTask(t("tasks.newBranch", { name }), () =>
gitNewBranch(folderPath, name)
)
}
function handleOpenWorktreeDialog() {
@@ -254,7 +262,7 @@ export function BranchDropdown({
const wtPath = worktreePath.trim()
if (!name || !wtPath) return
setWorktreeOpen(false)
await runGitTask(`新建工作树 ${name}`, async () => {
await runGitTask(t("tasks.newWorktree", { name }), async () => {
await gitWorktreeAdd(folderPath, name, wtPath)
await openFolderWindow(wtPath)
await setFolderParentBranch(wtPath, branch)
@@ -268,7 +276,7 @@ export function BranchDropdown({
async function handleCheckout(branchName: string) {
setDropdownOpen(false)
await runGitTask(`切换到 ${branchName}`, () =>
await runGitTask(t("tasks.checkoutTo", { branchName }), () =>
gitCheckout(folderPath, branchName)
)
}
@@ -276,7 +284,7 @@ export function BranchDropdown({
async function handleCheckoutRemote(remoteBranch: string) {
const localName = remoteBranch.replace(/^[^/]+\//, "")
setDropdownOpen(false)
await runGitTask(`切换到 ${localName}`, () =>
await runGitTask(t("tasks.checkoutTo", { branchName: localName }), () =>
gitCheckout(folderPath, localName)
)
}
@@ -289,23 +297,23 @@ export function BranchDropdown({
switch (type) {
case "merge":
await runGitTask(
`合并 ${branchName}`,
t("tasks.mergeBranch", { branchName }),
() => gitMerge(folderPath, branchName),
(result) => {
if (result.merged_commits === 0) {
return `${branchName} 没有新的提交`
return t("toasts.mergeNoNewCommits", { branchName })
}
return `已合并 ${result.merged_commits} 个提交`
return t("toasts.mergedCommits", { count: result.merged_commits })
}
)
break
case "rebase":
await runGitTask(`变基到 ${branchName}`, () =>
await runGitTask(t("tasks.rebaseTo", { branchName }), () =>
gitRebase(folderPath, branchName)
)
break
case "delete":
await runGitTask(`删除分支 ${branchName}`, () =>
await runGitTask(t("tasks.deleteBranch", { branchName }), () =>
gitDeleteBranch(folderPath, branchName)
)
break
@@ -316,11 +324,11 @@ export function BranchDropdown({
if (!confirmAction) return ""
switch (confirmAction.type) {
case "merge":
return "合并分支"
return t("confirm.mergeTitle")
case "rebase":
return "变基分支"
return t("confirm.rebaseTitle")
case "delete":
return "删除分支"
return t("confirm.deleteTitle")
}
}
@@ -328,11 +336,19 @@ export function BranchDropdown({
if (!confirmAction) return ""
switch (confirmAction.type) {
case "merge":
return `确定将 ${confirmAction.branchName} 合并到当前分支 ${branch} 吗?`
return t("confirm.mergeDescription", {
branchName: confirmAction.branchName,
currentBranch: branch ?? "-",
})
case "rebase":
return `确定将当前分支 ${branch} 变基到 ${confirmAction.branchName} 吗?`
return t("confirm.rebaseDescription", {
currentBranch: branch ?? "-",
branchName: confirmAction.branchName,
})
case "delete":
return `确定删除分支 ${confirmAction.branchName} 吗?此操作不可恢复。`
return t("confirm.deleteDescription", {
branchName: confirmAction.branchName,
})
}
}
@@ -351,7 +367,7 @@ export function BranchDropdown({
>
<BranchIcon className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{b}</span>
<span className="ml-auto text-xs"></span>
<span className="ml-auto text-xs">{t("current")}</span>
</div>
)
}
@@ -393,7 +409,7 @@ export function BranchDropdown({
}}
>
<GitBranch className="h-3.5 w-3.5" />
{t("switchToBranch")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
@@ -401,7 +417,11 @@ export function BranchDropdown({
setConfirmAction({ type: "merge", branchName: b })
}}
>
<GitMerge className="h-3.5 w-3.5" /> {b} {branch}
<GitMerge className="h-3.5 w-3.5" />
{t("mergeBranchIntoCurrent", {
branchName: b,
currentBranch: branch ?? "-",
})}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
@@ -409,8 +429,11 @@ export function BranchDropdown({
setConfirmAction({ type: "rebase", branchName: b })
}}
>
<GitPullRequestArrow className="h-3.5 w-3.5" /> {branch} {" "}
{b}
<GitPullRequestArrow className="h-3.5 w-3.5" />
{t("rebaseCurrentToBranch", {
currentBranch: branch ?? "-",
branchName: b,
})}
</DropdownMenuItem>
{!isRemote && (
<>
@@ -423,7 +446,7 @@ export function BranchDropdown({
}}
>
<Trash2 className="h-3.5 w-3.5" />
{t("deleteBranch")}
</DropdownMenuItem>
</>
)}
@@ -438,7 +461,7 @@ export function BranchDropdown({
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-1 text-sm tracking-tight hover:text-foreground/80 transition-colors outline-none cursor-default">
<GitFork className="h-3 w-3 shrink-0" />
<span className="truncate"></span>
<span className="truncate">{t("versionControl")}</span>
<ChevronDown className="h-3 w-3 shrink-0 opacity-50" />
</button>
</DropdownMenuTrigger>
@@ -446,11 +469,11 @@ export function BranchDropdown({
<DropdownMenuItem
disabled={loading}
onSelect={() =>
runGitTask("初始化 Git 仓库", () => gitInit(folderPath))
runGitTask(t("tasks.initGitRepo"), () => gitInit(folderPath))
}
>
<GitBranch className="h-3.5 w-3.5" />
Git
{t("initGitRepo")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -473,28 +496,30 @@ export function BranchDropdown({
disabled={loading}
onSelect={() =>
runGitTask(
"更新代码",
t("tasks.pullCode"),
() => gitPull(folderPath),
(result) => {
if (result.updated_files === 0) {
return "所有文件均为最新版本"
return t("toasts.allFilesUpToDate")
}
return `已更新 ${result.updated_files} 个文件`
return t("toasts.updatedFiles", {
count: result.updated_files,
})
}
)
}
>
<ArrowDownToLine className="h-3.5 w-3.5" />
{t("pullCode")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={loading}
onSelect={() =>
runGitTask("获取信息", () => gitFetch(folderPath))
runGitTask(t("tasks.fetchInfo"), () => gitFetch(folderPath))
}
>
<RefreshCw className="h-3.5 w-3.5" />
{t("fetchRemoteBranches")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
@@ -505,37 +530,42 @@ export function BranchDropdown({
if (!folder) return
setDropdownOpen(false)
openCommitWindow(folder.id).catch((err) => {
pushAlert("error", "打开提交窗口失败", String(err))
toast.error("打开提交窗口失败", { description: String(err) })
const title = t("toasts.openCommitWindowFailed")
pushAlert("error", title, String(err))
toast.error(title, { description: String(err) })
})
}}
>
<GitCommitHorizontal className="h-3.5 w-3.5" />
...
{t("openCommitWindow")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={loading}
onSelect={() =>
runGitTask(
"推送代码",
t("tasks.pushCode"),
() => gitPush(folderPath),
(result) => {
if (result.upstream_set) {
if (result.pushed_commits === 0) {
return "已设置远程跟踪分支"
return t("toasts.upstreamSet")
}
return `已设置远程跟踪分支并推送 ${result.pushed_commits} 个提交`
return t("toasts.upstreamSetAndPushed", {
count: result.pushed_commits,
})
}
if (result.pushed_commits === 0) {
return "没有可推送的提交"
return t("toasts.noCommitsToPush")
}
return `已推送 ${result.pushed_commits} 个提交`
return t("toasts.pushedCommits", {
count: result.pushed_commits,
})
}
)
}
>
<Upload className="h-3.5 w-3.5" />
...
{t("pushCode")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
@@ -548,14 +578,14 @@ export function BranchDropdown({
}}
>
<GitBranchPlus className="h-3.5 w-3.5" />
...
{t("newBranch")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={loading}
onSelect={handleOpenWorktreeDialog}
>
<FolderGit2 className="h-3.5 w-3.5" />
...
{t("newWorktree")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
@@ -563,20 +593,20 @@ export function BranchDropdown({
<DropdownMenuItem
disabled={loading}
onSelect={() =>
runGitTask("贮藏更改", () => gitStash(folderPath))
runGitTask(t("tasks.stashChanges"), () => gitStash(folderPath))
}
>
<Archive className="h-3.5 w-3.5" />
{t("stashChanges")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={loading}
onSelect={() =>
runGitTask("取消贮藏", () => gitStashPop(folderPath))
runGitTask(t("tasks.stashPop"), () => gitStashPop(folderPath))
}
>
<ArchiveRestore className="h-3.5 w-3.5" />
...
{t("stashPop")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
@@ -589,11 +619,13 @@ export function BranchDropdown({
<Collapsible open={localOpen} onOpenChange={setLocalOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
({branchList.local.length})
{t("localBranches", { count: branchList.local.length })}
</CollapsibleTrigger>
<CollapsibleContent>
{branchList.local.length === 0 ? (
<DropdownMenuItem disabled></DropdownMenuItem>
<DropdownMenuItem disabled>
{t("noLocalBranches")}
</DropdownMenuItem>
) : (
branchList.local.map((b) => renderBranchItem(b, false))
)}
@@ -603,11 +635,13 @@ export function BranchDropdown({
<Collapsible open={remoteOpen} onOpenChange={setRemoteOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
({branchList.remote.length})
{t("remoteBranches", { count: branchList.remote.length })}
</CollapsibleTrigger>
<CollapsibleContent>
{branchList.remote.length === 0 ? (
<DropdownMenuItem disabled></DropdownMenuItem>
<DropdownMenuItem disabled>
{t("noRemoteBranches")}
</DropdownMenuItem>
) : (
branchList.remote.map((b) => renderBranchItem(b, true))
)}
@@ -623,7 +657,7 @@ export function BranchDropdown({
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-orange-500 dark:text-orange-400 hover:bg-accent hover:text-orange-600 dark:hover:text-orange-300 transition-colors cursor-default select-none"
disabled={loading}
onClick={handleMergeParent}
title={`当前分支从 ${parentBranch} 创建,点击合并 ${parentBranch} 到当前分支`}
title={t("parentBranchHint", { parentBranch })}
>
<ArrowLeftRight className="h-3 w-3 shrink-0" />
<span className="truncate max-w-32">{parentBranch}</span>
@@ -644,14 +678,14 @@ export function BranchDropdown({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction
variant={
confirmAction?.type === "delete" ? "destructive" : "default"
}
onClick={handleConfirm}
>
{tCommon("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -660,13 +694,13 @@ export function BranchDropdown({
<Dialog open={newBranchOpen} onOpenChange={setNewBranchOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("dialogs.newBranchTitle")}</DialogTitle>
<DialogDescription>
{branch}
{t("dialogs.newBranchDescription", { branch: branch ?? "-" })}
</DialogDescription>
</DialogHeader>
<Input
placeholder="分支名称"
placeholder={t("dialogs.branchNamePlaceholder")}
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
onKeyDown={(e) => {
@@ -677,13 +711,13 @@ export function BranchDropdown({
/>
<DialogFooter>
<Button variant="outline" onClick={() => setNewBranchOpen(false)}>
{tCommon("cancel")}
</Button>
<Button
disabled={!newBranchName.trim() || loading}
onClick={handleNewBranch}
>
{tCommon("create")}
</Button>
</DialogFooter>
</DialogContent>
@@ -692,17 +726,17 @@ export function BranchDropdown({
<Dialog open={worktreeOpen} onOpenChange={setWorktreeOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("dialogs.newWorktreeTitle")}</DialogTitle>
<DialogDescription>
{branch}
{t("dialogs.newWorktreeDescription", { branch: branch ?? "-" })}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label htmlFor="wt-branch"></Label>
<Label htmlFor="wt-branch">{t("dialogs.branchNameLabel")}</Label>
<Input
id="wt-branch"
placeholder="分支名称"
placeholder={t("dialogs.branchNamePlaceholder")}
value={worktreeBranchName}
onChange={(e) => handleWorktreeBranchChange(e.target.value)}
onKeyDown={(e) => {
@@ -713,11 +747,11 @@ export function BranchDropdown({
/>
</div>
<div className="space-y-2">
<Label htmlFor="wt-path"></Label>
<Label htmlFor="wt-path">{t("dialogs.worktreePathLabel")}</Label>
<div className="flex gap-2">
<Input
id="wt-path"
placeholder="工作树路径"
placeholder={t("dialogs.worktreePathPlaceholder")}
value={worktreePath}
onChange={(e) => setWorktreePath(e.target.value)}
className="flex-1"
@@ -734,7 +768,7 @@ export function BranchDropdown({
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setWorktreeOpen(false)}>
{tCommon("cancel")}
</Button>
<Button
disabled={
@@ -742,7 +776,7 @@ export function BranchDropdown({
}
onClick={handleNewWorktree}
>
{tCommon("create")}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -3,6 +3,7 @@
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { useState, useEffect, useCallback, useMemo, useRef } from "react"
import { ChevronDown, Play, Plus, Square } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
@@ -40,6 +41,7 @@ function setSelectedCommandId(folderId: number, cmdId: number) {
}
export function CommandDropdown() {
const t = useTranslations("Folder.commandDropdown")
const { folder } = useFolderContext()
const { createTerminalWithCommand } = useTerminalContext()
const [commands, setCommands] = useState<FolderCommand[]>([])
@@ -273,7 +275,7 @@ export function CommandDropdown() {
if (!folder) return null
// No commands → show "Add Command"
// No commands → show add command button
if (commands.length === 0) {
return (
<>
@@ -285,7 +287,7 @@ export function CommandDropdown() {
disabled={bootstrapping}
>
<Plus className="h-3 w-3" />
{bootstrapping ? "Loading..." : "Add Command"}
{bootstrapping ? t("loading") : t("addCommand")}
</Button>
<CommandManageDialog
open={manageOpen}
@@ -326,7 +328,7 @@ export function CommandDropdown() {
))}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setManageOpen(true)}>
Manage Commands...
{t("manageCommands")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -341,8 +343,8 @@ export function CommandDropdown() {
onClick={handleRunOrStop}
title={
isActiveCommandRunning
? `Stop: ${activeCmd?.command}`
: `Run: ${activeCmd?.command}`
? t("stopCommandTitle", { command: activeCmd?.command ?? "" })
: t("runCommandTitle", { command: activeCmd?.command ?? "" })
}
>
{isActiveCommandRunning ? (

View File

@@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import { useTranslations } from "next-intl"
import {
Dialog,
DialogContent,
@@ -41,6 +42,8 @@ export function CommandManageDialog({
commands,
onSaved,
}: CommandManageDialogProps) {
const t = useTranslations("Folder.commandDropdown.manageDialog")
const tCommon = useTranslations("Folder.common")
const [drafts, setDrafts] = useState<CommandDraft[]>([])
const [saving, setSaving] = useState(false)
@@ -115,13 +118,13 @@ export function CommandManageDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Manage Commands</DialogTitle>
<DialogTitle>{t("title")}</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-72">
<div className="space-y-2 pr-2">
{visibleDrafts.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No commands yet
{t("empty")}
</p>
)}
{drafts.map(
@@ -129,7 +132,7 @@ export function CommandManageDialog({
!draft.deleted && (
<div key={index} className="flex items-center gap-2">
<Input
placeholder="Name"
placeholder={t("namePlaceholder")}
value={draft.name}
onChange={(e) =>
updateDraft(index, "name", e.target.value)
@@ -137,7 +140,7 @@ export function CommandManageDialog({
className="h-8 text-sm flex-1"
/>
<Input
placeholder="Command"
placeholder={t("commandPlaceholder")}
value={draft.command}
onChange={(e) =>
updateDraft(index, "command", e.target.value)
@@ -160,7 +163,7 @@ export function CommandManageDialog({
<DialogFooter className="flex items-center justify-between sm:justify-between">
<Button variant="outline" size="sm" onClick={addDraft}>
<Plus className="h-3.5 w-3.5 mr-1" />
Add
{t("add")}
</Button>
<div className="flex gap-2">
<Button
@@ -168,10 +171,10 @@ export function CommandManageDialog({
size="sm"
onClick={() => onOpenChange(false)}
>
Cancel
{tCommon("cancel")}
</Button>
<Button size="sm" onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save"}
{saving ? t("saving") : tCommon("save")}
</Button>
</div>
</DialogFooter>

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>

View File

@@ -711,6 +711,317 @@
"jumpToLine": "Jump to line {line}",
"noParsedDiffSections": "No parsed diff sections",
"loadingEditor": "Loading editor..."
},
"branchDropdown": {
"toasts": {
"commitCodeCompleted": "Code commit completed",
"committedFiles": "Committed {count, plural, one {# file} other {# files}}",
"taskCompleted": "{label} completed",
"taskFailed": "{label} failed",
"mergeNoNewCommits": "{branchName} has no new commits",
"mergedCommits": "Merged {count, plural, one {# commit} other {# commits}}",
"allFilesUpToDate": "All files are up to date",
"updatedFiles": "Updated {count, plural, one {# file} other {# files}}",
"openCommitWindowFailed": "Failed to open commit window",
"upstreamSet": "Upstream branch has been set",
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
"noCommitsToPush": "No commits to push",
"pushedCommits": "Pushed {count, plural, one {# commit} other {# commits}}"
},
"tasks": {
"newBranch": "Create branch {name}",
"newWorktree": "Create worktree {name}",
"checkoutTo": "Checkout to {branchName}",
"mergeBranch": "Merge {branchName}",
"rebaseTo": "Rebase to {branchName}",
"deleteBranch": "Delete branch {branchName}",
"initGitRepo": "Initialize Git repository",
"pullCode": "Pull code",
"fetchInfo": "Fetch info",
"pushCode": "Push code",
"stashChanges": "Stash changes",
"stashPop": "Pop stash"
},
"confirm": {
"mergeTitle": "Merge branch",
"rebaseTitle": "Rebase branch",
"deleteTitle": "Delete branch",
"mergeDescription": "Merge {branchName} into current branch {currentBranch}?",
"rebaseDescription": "Rebase current branch {currentBranch} onto {branchName}?",
"deleteDescription": "Delete branch {branchName}? This action cannot be undone."
},
"current": "Current",
"switchToBranch": "Switch to this branch",
"mergeBranchIntoCurrent": "Merge {branchName} into {currentBranch}",
"rebaseCurrentToBranch": "Rebase {currentBranch} onto {branchName}",
"deleteBranch": "Delete branch",
"versionControl": "Version Control",
"initGitRepo": "Initialize Git repository",
"pullCode": "Pull code",
"fetchRemoteBranches": "Fetch remote branches",
"openCommitWindow": "Commit code...",
"pushCode": "Push...",
"newBranch": "New branch...",
"newWorktree": "New worktree...",
"stashChanges": "Stash changes",
"stashPop": "Pop stash...",
"localBranches": "Local branches ({count, plural, one {#} other {#}})",
"noLocalBranches": "No local branches",
"remoteBranches": "Remote branches ({count, plural, one {#} other {#}})",
"noRemoteBranches": "No remote branches",
"parentBranchHint": "Current branch was created from {parentBranch}. Click to merge {parentBranch} into current branch.",
"dialogs": {
"newBranchTitle": "New branch",
"newBranchDescription": "Create a new branch from current branch {branch}",
"branchNamePlaceholder": "Branch name",
"newWorktreeTitle": "New worktree",
"newWorktreeDescription": "Create a new worktree from current branch {branch}",
"branchNameLabel": "Branch name",
"worktreePathLabel": "Worktree path",
"worktreePathPlaceholder": "Worktree path"
}
},
"commitDialog": {
"toasts": {
"commitCompleted": "Code commit completed",
"committedFiles": "Committed {count, plural, one {# file} other {# files}}",
"addedToVcs": "Added to VCS",
"addToVcsFailed": "Failed to add to VCS",
"fileDeleted": "File deleted",
"deleteFailed": "Delete failed",
"fileRolledBack": "File rolled back",
"rollbackFailed": "Rollback failed"
},
"confirm": {
"deleteTitle": "Confirm deletion",
"deleteDescription": "Delete file \"{file}\"? This action cannot be undone.",
"rollbackTitle": "Confirm rollback",
"rollbackDescription": "Rollback file \"{file}\" to HEAD? Unsaved changes will be lost."
},
"actions": {
"select": "Select",
"unselect": "Unselect",
"rollback": "Rollback",
"addToVcs": "Add to VCS"
},
"aria": {
"selectFile": "{action} {path}",
"unselectAllFiles": "Unselect all files",
"selectAllFiles": "Select all files",
"unselectTracked": "Unselect tracked changes",
"selectTracked": "Select tracked changes",
"unselectUntracked": "Unselect untracked files",
"selectUntracked": "Select untracked files"
},
"loading": "Loading...",
"selectionCount": "{selected} / {total} files",
"emptyFiles": "No changed files",
"trackedChanges": "Tracked changes ({count})",
"untrackedFiles": "Untracked files ({count})",
"commitMessage": "Commit message",
"commitMessagePlaceholder": "Enter commit message...",
"commitButton": "Commit ({count})",
"head": "HEAD",
"workingTree": "Working Tree",
"clickFileToDiff": "Click a file name to view diff",
"loadingDiff": "Loading diff..."
},
"gitLogTab": {
"filesTitle": "Files",
"expandAllFiles": "Expand all files",
"collapseAllFiles": "Collapse all files",
"workspace": "workspace",
"retry": "Retry",
"noCommitsFound": "No commits found",
"hash": "Hash",
"copyHash": "Copy hash",
"author": "Author",
"noFileChangeDetails": "No file change details available.",
"branchesTitle": "Branches",
"loadingBranches": "Loading branches...",
"noContainingBranches": "No containing branches found.",
"newBranch": "New branch...",
"viewCommitDiffAria": "View diff for commit {hash}",
"copyFullCommitHashAria": "Copy full commit hash {hash}",
"pushStatus": {
"pushed": "Pushed to remote",
"notPushed": "Not pushed to remote",
"unknown": "Push status unknown (no upstream configured)"
},
"time": {
"monthsAgo": "{count, plural, one {# month ago} other {# months ago}}",
"daysAgo": "{count, plural, one {# day ago} other {# days ago}}",
"hoursAgo": "{count, plural, one {# hour ago} other {# hours ago}}",
"minsAgo": "{count, plural, one {# min ago} other {# mins ago}}",
"justNow": "just now"
},
"toasts": {
"createdAndSwitchedNewBranch": "Created and switched to new branch",
"newBranchFromCommit": "{name} (from {shortHash})",
"createBranchFailed": "Failed to create branch"
},
"branchSelector": {
"selectBranchPlaceholder": "Select branch...",
"localBranches": "Local branches",
"current": "Current",
"remoteBranches": "Remote branches",
"refreshCommitHistory": "Refresh commit history"
},
"dialogs": {
"newBranchTitle": "New branch",
"newBranchDescription": "Create a new branch with commit {shortHash} as the latest commit.",
"branchNamePlaceholder": "Branch name"
}
},
"gitChangesTab": {
"workspace": "workspace",
"noChanges": "No local changes",
"trackedChanges": "Tracked changes ({count})",
"untrackedFiles": "Untracked files ({count})",
"expandTracked": "Expand tracked changes",
"collapseTracked": "Collapse tracked changes",
"expandUntracked": "Expand untracked files",
"collapseUntracked": "Collapse untracked files",
"actions": {
"rollback": "Rollback",
"addToVcs": "Add to VCS"
},
"toasts": {
"noAddableFilesInDir": "No changed files in this directory can be added to VCS",
"noRollbackFilesInDir": "No changed files in this directory can be rolled back",
"addedToVcs": "Added {name} to VCS",
"addToVcsFailed": "Failed to add to VCS",
"rolledBack": "Rolled back {name}",
"rollbackFailed": "Rollback failed",
"addedFilesToVcs": "Added {count, plural, one {# file} other {# files}} to VCS",
"rolledBackFiles": "Rolled back {count, plural, one {# file} other {# files}}"
},
"directoryDialog": {
"descriptionAdd": "Select files under directory {path} to add to VCS.",
"descriptionRollback": "Select files under directory {path} to roll back.",
"descriptionFallback": "Select files to proceed.",
"selectionCount": "Selected {selected} / {total} files",
"selectAll": "Select all",
"unselectAll": "Unselect all",
"loadingCandidates": "Loading directory changes...",
"noOperableFiles": "No operable files"
},
"rollbackConfirm": {
"title": "Confirm rollback",
"descriptionWithTarget": "Roll back local changes for {kind} \"{name}\"?",
"descriptionFallback": "Roll back local changes?",
"kindDirectory": "directory",
"kindFile": "file"
}
},
"fileTreeTab": {
"workspace": "Workspace",
"retry": "Retry",
"git": "Git",
"openInFileManager": "Open in file manager",
"openInFinder": "Open in Finder",
"openInExplorer": "Open in Explorer",
"attachToCurrentSession": "Attach to current session",
"compareWithBranch": "Compare with branch...",
"reloadFromDisk": "Reload from disk",
"openIn": "Open in",
"openInTerminal": "Open in terminal",
"actions": {
"select": "Select",
"unselect": "Unselect",
"rollback": "Rollback",
"addToVcs": "Add to VCS"
},
"aria": {
"selectPath": "{action} {path}"
},
"toasts": {
"openDirectoryFailed": "Failed to open directory",
"openBuiltinTerminalFailed": "Unable to open built-in terminal",
"noAddableFilesInDir": "No changed files in this directory can be added to VCS",
"noRollbackFilesInDir": "No changed files in this directory can be rolled back",
"addedToVcs": "Added {name} to VCS",
"addToVcsFailed": "Failed to add to VCS",
"loadBranchesFailed": "Failed to load branches",
"renameFailed": "Rename failed",
"deleteFailed": "Delete failed",
"rolledBack": "Rolled back {name}",
"rollbackFailed": "Rollback failed",
"addedFilesToVcs": "Added {count, plural, one {# file} other {# files}} to VCS",
"rolledBackFiles": "Rolled back {count, plural, one {# file} other {# files}}",
"savedAsCopy": "Saved as a copy",
"saveCopyFailed": "Failed to save as copy",
"watchStartFailed": "Failed to start file watch"
},
"renameDialog": {
"renameDirectory": "Rename directory",
"renameFile": "Rename file",
"description": "Enter a new name (name only, no path).",
"placeholderDirectory": "new-folder-name",
"placeholderFile": "new-file-name.ext"
},
"directoryDialog": {
"descriptionAdd": "Select files under directory {path} to add to VCS.",
"descriptionRollback": "Select files under directory {path} to roll back.",
"descriptionFallback": "Select files to proceed.",
"selectionCount": "Selected {selected} / {total} files",
"selectAll": "Select all",
"unselectAll": "Unselect all",
"loadingCandidates": "Loading directory changes...",
"noOperableFiles": "No operable files"
},
"compareDialog": {
"title": "Compare with branch",
"descriptionWithTarget": "Select a branch and compare with {kind} {path}",
"descriptionFallback": "Select a branch to compare.",
"kindDirectory": "directory",
"kindFile": "file",
"filterPlaceholder": "Filter branches, e.g. main / origin/main",
"singleClickHint": "Click a branch to compare directly",
"loadingBranches": "Loading branches...",
"recentBranches": "Recent branches ({count})",
"noCurrentBranch": "No current branch",
"localBranches": "Local branches ({count})",
"remoteBranches": "Remote branches ({count})",
"noMatchingBranches": "No matching branches"
},
"externalConflictDialog": {
"title": "External file changes detected",
"descriptionWithPath": "File {path} has changed on disk, and current edits are unsaved.",
"descriptionFallback": "Current file has changed on disk, and current edits are unsaved.",
"compare": "Compare",
"savingCopy": "Saving copy...",
"saveAsCopy": "Save as copy",
"reload": "Reload"
},
"deleteConfirm": {
"title": "Confirm deletion",
"descriptionWithTarget": "Delete {kind} \"{name}\"? This action cannot be undone.",
"descriptionFallback": "This action cannot be undone.",
"kindDirectory": "directory",
"kindFile": "file"
},
"rollbackConfirm": {
"title": "Confirm rollback",
"descriptionWithTarget": "Rollback local changes for file \"{name}\"?",
"descriptionFallback": "Rollback local changes for this file?"
},
"terminalTitle": "Terminal · {name}"
},
"commandDropdown": {
"loading": "Loading...",
"addCommand": "Add Command",
"manageCommands": "Manage Commands...",
"runCommandTitle": "Run: {command}",
"stopCommandTitle": "Stop: {command}",
"manageDialog": {
"title": "Manage Commands",
"empty": "No commands yet",
"namePlaceholder": "Name",
"commandPlaceholder": "Command",
"add": "Add",
"saving": "Saving..."
}
}
}
}

View File

@@ -711,6 +711,317 @@
"jumpToLine": "跳转到第 {line} 行",
"noParsedDiffSections": "未解析到差异区块",
"loadingEditor": "编辑器加载中..."
},
"branchDropdown": {
"toasts": {
"commitCodeCompleted": "提交代码完成",
"committedFiles": "已提交 {count} 个文件",
"taskCompleted": "{label} 完成",
"taskFailed": "{label} 失败",
"mergeNoNewCommits": "{branchName} 没有新的提交",
"mergedCommits": "已合并 {count} 个提交",
"allFilesUpToDate": "所有文件均为最新版本",
"updatedFiles": "已更新 {count} 个文件",
"openCommitWindowFailed": "打开提交窗口失败",
"upstreamSet": "已设置远程跟踪分支",
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
"noCommitsToPush": "没有可推送的提交",
"pushedCommits": "已推送 {count} 个提交"
},
"tasks": {
"newBranch": "新建分支 {name}",
"newWorktree": "新建工作树 {name}",
"checkoutTo": "切换到 {branchName}",
"mergeBranch": "合并 {branchName}",
"rebaseTo": "变基到 {branchName}",
"deleteBranch": "删除分支 {branchName}",
"initGitRepo": "初始化 Git 仓库",
"pullCode": "更新代码",
"fetchInfo": "获取信息",
"pushCode": "推送代码",
"stashChanges": "贮藏更改",
"stashPop": "取消贮藏"
},
"confirm": {
"mergeTitle": "合并分支",
"rebaseTitle": "变基分支",
"deleteTitle": "删除分支",
"mergeDescription": "确定将 {branchName} 合并到当前分支 {currentBranch} 吗?",
"rebaseDescription": "确定将当前分支 {currentBranch} 变基到 {branchName} 吗?",
"deleteDescription": "确定删除分支 {branchName} 吗?此操作不可恢复。"
},
"current": "当前",
"switchToBranch": "切换到此分支",
"mergeBranchIntoCurrent": "将 {branchName} 合并到 {currentBranch}",
"rebaseCurrentToBranch": "将 {currentBranch} 变基到 {branchName}",
"deleteBranch": "删除分支",
"versionControl": "版本控制",
"initGitRepo": "初始化 Git 仓库",
"pullCode": "更新代码",
"fetchRemoteBranches": "提取远程分支",
"openCommitWindow": "提交代码...",
"pushCode": "推送...",
"newBranch": "新建分支...",
"newWorktree": "新建工作树...",
"stashChanges": "贮藏更改",
"stashPop": "取消贮藏...",
"localBranches": "本地分支 ({count})",
"noLocalBranches": "无本地分支",
"remoteBranches": "远程分支 ({count})",
"noRemoteBranches": "无远程分支",
"parentBranchHint": "当前分支从 {parentBranch} 创建,点击合并 {parentBranch} 到当前分支",
"dialogs": {
"newBranchTitle": "新建分支",
"newBranchDescription": "从当前分支 {branch} 创建新分支",
"branchNamePlaceholder": "分支名称",
"newWorktreeTitle": "新建工作树",
"newWorktreeDescription": "从当前分支 {branch} 创建新的工作树",
"branchNameLabel": "分支名称",
"worktreePathLabel": "工作树路径",
"worktreePathPlaceholder": "工作树路径"
}
},
"commitDialog": {
"toasts": {
"commitCompleted": "提交代码完成",
"committedFiles": "已提交 {count} 个文件",
"addedToVcs": "已添加到 VCS",
"addToVcsFailed": "添加到 VCS 失败",
"fileDeleted": "文件已删除",
"deleteFailed": "删除失败",
"fileRolledBack": "文件已回滚",
"rollbackFailed": "回滚失败"
},
"confirm": {
"deleteTitle": "确认删除",
"deleteDescription": "确定要删除文件「{file}」吗?此操作不可恢复。",
"rollbackTitle": "确认回滚",
"rollbackDescription": "确定要回滚文件「{file}」到 HEAD 版本吗?未保存的修改将丢失。"
},
"actions": {
"select": "选择",
"unselect": "取消选择",
"rollback": "回滚",
"addToVcs": "添加到 VCS"
},
"aria": {
"selectFile": "{action} {path}",
"unselectAllFiles": "取消选择全部文件",
"selectAllFiles": "选择全部文件",
"unselectTracked": "取消选择已跟踪改动",
"selectTracked": "选择已跟踪改动",
"unselectUntracked": "取消选择未跟踪文件",
"selectUntracked": "选择未跟踪文件"
},
"loading": "加载中...",
"selectionCount": "{selected} / {total} 个文件",
"emptyFiles": "没有改动的文件",
"trackedChanges": "已跟踪改动 ({count})",
"untrackedFiles": "未跟踪文件 ({count})",
"commitMessage": "提交消息",
"commitMessagePlaceholder": "输入提交信息...",
"commitButton": "提交 ({count})",
"head": "HEAD",
"workingTree": "Working Tree",
"clickFileToDiff": "点击文件名查看差异",
"loadingDiff": "加载差异..."
},
"gitLogTab": {
"filesTitle": "Files",
"expandAllFiles": "展开全部文件",
"collapseAllFiles": "折叠全部文件",
"workspace": "工作区",
"retry": "重试",
"noCommitsFound": "未找到提交记录",
"hash": "Hash",
"copyHash": "复制哈希",
"author": "作者",
"noFileChangeDetails": "暂无文件变更详情。",
"branchesTitle": "分支",
"loadingBranches": "正在加载分支...",
"noContainingBranches": "未找到包含此提交的分支。",
"newBranch": "新建分支...",
"viewCommitDiffAria": "查看提交 {hash} 的差异",
"copyFullCommitHashAria": "复制完整提交哈希 {hash}",
"pushStatus": {
"pushed": "已推送到远程",
"notPushed": "未推送到远程",
"unknown": "推送状态未知(未配置上游分支)"
},
"time": {
"monthsAgo": "{count} 个月前",
"daysAgo": "{count} 天前",
"hoursAgo": "{count} 小时前",
"minsAgo": "{count} 分钟前",
"justNow": "刚刚"
},
"toasts": {
"createdAndSwitchedNewBranch": "已创建并切换到新分支",
"newBranchFromCommit": "{name}(来自 {shortHash}",
"createBranchFailed": "新建分支失败"
},
"branchSelector": {
"selectBranchPlaceholder": "选择分支...",
"localBranches": "本地分支",
"current": "当前",
"remoteBranches": "远程分支",
"refreshCommitHistory": "刷新提交记录"
},
"dialogs": {
"newBranchTitle": "新建分支",
"newBranchDescription": "以提交 {shortHash} 作为最后提交创建新分支。",
"branchNamePlaceholder": "分支名称"
}
},
"gitChangesTab": {
"workspace": "工作区",
"noChanges": "暂无本地改动",
"trackedChanges": "本地已跟踪改动 ({count})",
"untrackedFiles": "本地未跟踪文件 ({count})",
"expandTracked": "展开已跟踪改动",
"collapseTracked": "折叠已跟踪改动",
"expandUntracked": "展开未跟踪文件",
"collapseUntracked": "折叠未跟踪文件",
"actions": {
"rollback": "回滚",
"addToVcs": "添加到 VCS"
},
"toasts": {
"noAddableFilesInDir": "该目录下没有可添加到 VCS 的变更文件",
"noRollbackFilesInDir": "该目录下没有可回滚的变更文件",
"addedToVcs": "已添加 {name} 到 VCS",
"addToVcsFailed": "添加到 VCS 失败",
"rolledBack": "已回滚 {name}",
"rollbackFailed": "回滚失败",
"addedFilesToVcs": "已添加 {count} 个文件到 VCS",
"rolledBackFiles": "已回滚 {count} 个文件"
},
"directoryDialog": {
"descriptionAdd": "选择目录 {path} 下要添加到 VCS 的文件。",
"descriptionRollback": "选择目录 {path} 下要回滚的文件。",
"descriptionFallback": "选择要操作的文件。",
"selectionCount": "已选择 {selected} / {total} 个文件",
"selectAll": "全选",
"unselectAll": "取消全选",
"loadingCandidates": "正在加载目录变更...",
"noOperableFiles": "没有可操作的文件"
},
"rollbackConfirm": {
"title": "确认回滚",
"descriptionWithTarget": "确定回滚{kind}「{name}」的本地修改吗?",
"descriptionFallback": "确定回滚本地修改吗?",
"kindDirectory": "目录",
"kindFile": "文件"
}
},
"fileTreeTab": {
"workspace": "工作区",
"retry": "重试",
"git": "Git",
"openInFileManager": "在文件管理器打开",
"openInFinder": "在访达打开",
"openInExplorer": "在资源管理器打开",
"attachToCurrentSession": "附加到当前会话",
"compareWithBranch": "与分支比较...",
"reloadFromDisk": "从磁盘重新加载",
"openIn": "打开于",
"openInTerminal": "在终端打开",
"actions": {
"select": "选择",
"unselect": "取消选择",
"rollback": "回滚",
"addToVcs": "添加到 VCS"
},
"aria": {
"selectPath": "{action} {path}"
},
"toasts": {
"openDirectoryFailed": "打开目录失败",
"openBuiltinTerminalFailed": "无法打开内置终端",
"noAddableFilesInDir": "该目录下没有可添加到 VCS 的变更文件",
"noRollbackFilesInDir": "该目录下没有可回滚的变更文件",
"addedToVcs": "已添加 {name} 到 VCS",
"addToVcsFailed": "添加到 VCS 失败",
"loadBranchesFailed": "加载分支失败",
"renameFailed": "重命名失败",
"deleteFailed": "删除失败",
"rolledBack": "已回滚 {name}",
"rollbackFailed": "回滚失败",
"addedFilesToVcs": "已添加 {count} 个文件到 VCS",
"rolledBackFiles": "已回滚 {count} 个文件",
"savedAsCopy": "已另存为副本",
"saveCopyFailed": "另存为副本失败",
"watchStartFailed": "文件监听启动失败"
},
"renameDialog": {
"renameDirectory": "重命名目录",
"renameFile": "重命名文件",
"description": "输入新的名称(仅名称,不含路径)。",
"placeholderDirectory": "new-folder-name",
"placeholderFile": "new-file-name.ext"
},
"directoryDialog": {
"descriptionAdd": "选择目录 {path} 下要添加到 VCS 的文件。",
"descriptionRollback": "选择目录 {path} 下要回滚的文件。",
"descriptionFallback": "选择要操作的文件。",
"selectionCount": "已选择 {selected} / {total} 个文件",
"selectAll": "全选",
"unselectAll": "取消全选",
"loadingCandidates": "正在加载目录变更...",
"noOperableFiles": "没有可操作的文件"
},
"compareDialog": {
"title": "与分支比较",
"descriptionWithTarget": "选择分支并与{kind} {path} 对比",
"descriptionFallback": "选择要比较的分支。",
"kindDirectory": "目录",
"kindFile": "文件",
"filterPlaceholder": "过滤分支,例如 main / origin/main",
"singleClickHint": "单击分支即可直接比较",
"loadingBranches": "正在加载分支...",
"recentBranches": "最近分支 ({count})",
"noCurrentBranch": "无当前分支",
"localBranches": "本地分支 ({count})",
"remoteBranches": "远程分支 ({count})",
"noMatchingBranches": "无匹配分支"
},
"externalConflictDialog": {
"title": "检测到外部文件变更",
"descriptionWithPath": "文件 {path} 在磁盘已发生变化,当前编辑内容尚未保存。",
"descriptionFallback": "当前文件在磁盘已发生变化,当前编辑内容尚未保存。",
"compare": "对比",
"savingCopy": "另存中...",
"saveAsCopy": "另存为副本",
"reload": "重载"
},
"deleteConfirm": {
"title": "确认删除",
"descriptionWithTarget": "确定删除{kind} \"{name}\" 吗?此操作不可撤销。",
"descriptionFallback": "此操作不可撤销。",
"kindDirectory": "目录",
"kindFile": "文件"
},
"rollbackConfirm": {
"title": "确认回滚",
"descriptionWithTarget": "确定回滚文件 \"{name}\" 的本地修改吗?",
"descriptionFallback": "确定回滚该文件的本地修改吗?"
},
"terminalTitle": "终端 · {name}"
},
"commandDropdown": {
"loading": "加载中...",
"addCommand": "添加命令",
"manageCommands": "管理命令...",
"runCommandTitle": "运行:{command}",
"stopCommandTitle": "停止:{command}",
"manageDialog": {
"title": "管理命令",
"empty": "暂无命令",
"namePlaceholder": "名称",
"commandPlaceholder": "命令",
"add": "添加",
"saving": "保存中..."
}
}
}
}

View File

@@ -711,6 +711,317 @@
"jumpToLine": "跳到第 {line} 行",
"noParsedDiffSections": "未解析到差異區塊",
"loadingEditor": "編輯器載入中..."
},
"branchDropdown": {
"toasts": {
"commitCodeCompleted": "提交程式碼完成",
"committedFiles": "已提交 {count} 個檔案",
"taskCompleted": "{label} 完成",
"taskFailed": "{label} 失敗",
"mergeNoNewCommits": "{branchName} 沒有新的提交",
"mergedCommits": "已合併 {count} 個提交",
"allFilesUpToDate": "所有檔案均為最新版本",
"updatedFiles": "已更新 {count} 個檔案",
"openCommitWindowFailed": "打開提交視窗失敗",
"upstreamSet": "已設定遠端追蹤分支",
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
"noCommitsToPush": "沒有可推送的提交",
"pushedCommits": "已推送 {count} 個提交"
},
"tasks": {
"newBranch": "新增分支 {name}",
"newWorktree": "新增工作樹 {name}",
"checkoutTo": "切換到 {branchName}",
"mergeBranch": "合併 {branchName}",
"rebaseTo": "變基到 {branchName}",
"deleteBranch": "刪除分支 {branchName}",
"initGitRepo": "初始化 Git 倉庫",
"pullCode": "更新程式碼",
"fetchInfo": "獲取資訊",
"pushCode": "推送程式碼",
"stashChanges": "暫存變更",
"stashPop": "取消暫存"
},
"confirm": {
"mergeTitle": "合併分支",
"rebaseTitle": "變基分支",
"deleteTitle": "刪除分支",
"mergeDescription": "確定將 {branchName} 合併到目前分支 {currentBranch} 嗎?",
"rebaseDescription": "確定將目前分支 {currentBranch} 變基到 {branchName} 嗎?",
"deleteDescription": "確定刪除分支 {branchName} 嗎?此操作無法復原。"
},
"current": "目前",
"switchToBranch": "切換到此分支",
"mergeBranchIntoCurrent": "將 {branchName} 合併到 {currentBranch}",
"rebaseCurrentToBranch": "將 {currentBranch} 變基到 {branchName}",
"deleteBranch": "刪除分支",
"versionControl": "版本控制",
"initGitRepo": "初始化 Git 倉庫",
"pullCode": "更新程式碼",
"fetchRemoteBranches": "提取遠端分支",
"openCommitWindow": "提交程式碼...",
"pushCode": "推送...",
"newBranch": "新增分支...",
"newWorktree": "新增工作樹...",
"stashChanges": "暫存變更",
"stashPop": "取消暫存...",
"localBranches": "本地分支 ({count})",
"noLocalBranches": "無本地分支",
"remoteBranches": "遠端分支 ({count})",
"noRemoteBranches": "無遠端分支",
"parentBranchHint": "目前分支從 {parentBranch} 建立,點擊合併 {parentBranch} 到目前分支",
"dialogs": {
"newBranchTitle": "新增分支",
"newBranchDescription": "從目前分支 {branch} 建立新分支",
"branchNamePlaceholder": "分支名稱",
"newWorktreeTitle": "新增工作樹",
"newWorktreeDescription": "從目前分支 {branch} 建立新的工作樹",
"branchNameLabel": "分支名稱",
"worktreePathLabel": "工作樹路徑",
"worktreePathPlaceholder": "工作樹路徑"
}
},
"commitDialog": {
"toasts": {
"commitCompleted": "提交程式碼完成",
"committedFiles": "已提交 {count} 個檔案",
"addedToVcs": "已加入到 VCS",
"addToVcsFailed": "加入到 VCS 失敗",
"fileDeleted": "檔案已刪除",
"deleteFailed": "刪除失敗",
"fileRolledBack": "檔案已回滾",
"rollbackFailed": "回滾失敗"
},
"confirm": {
"deleteTitle": "確認刪除",
"deleteDescription": "確定要刪除檔案「{file}」嗎?此操作無法復原。",
"rollbackTitle": "確認回滾",
"rollbackDescription": "確定要回滾檔案「{file}」到 HEAD 版本嗎?未儲存修改將遺失。"
},
"actions": {
"select": "選擇",
"unselect": "取消選擇",
"rollback": "回滾",
"addToVcs": "加入到 VCS"
},
"aria": {
"selectFile": "{action} {path}",
"unselectAllFiles": "取消選擇全部檔案",
"selectAllFiles": "選擇全部檔案",
"unselectTracked": "取消選擇已追蹤變更",
"selectTracked": "選擇已追蹤變更",
"unselectUntracked": "取消選擇未追蹤檔案",
"selectUntracked": "選擇未追蹤檔案"
},
"loading": "載入中...",
"selectionCount": "{selected} / {total} 個檔案",
"emptyFiles": "沒有變更的檔案",
"trackedChanges": "已追蹤變更 ({count})",
"untrackedFiles": "未追蹤檔案 ({count})",
"commitMessage": "提交訊息",
"commitMessagePlaceholder": "輸入提交訊息...",
"commitButton": "提交 ({count})",
"head": "HEAD",
"workingTree": "Working Tree",
"clickFileToDiff": "點擊檔案名稱查看差異",
"loadingDiff": "載入差異中..."
},
"gitLogTab": {
"filesTitle": "Files",
"expandAllFiles": "展開全部檔案",
"collapseAllFiles": "折疊全部檔案",
"workspace": "工作區",
"retry": "重試",
"noCommitsFound": "未找到提交記錄",
"hash": "Hash",
"copyHash": "複製雜湊",
"author": "作者",
"noFileChangeDetails": "暫無檔案變更詳情。",
"branchesTitle": "分支",
"loadingBranches": "正在載入分支...",
"noContainingBranches": "未找到包含此提交的分支。",
"newBranch": "新增分支...",
"viewCommitDiffAria": "查看提交 {hash} 的差異",
"copyFullCommitHashAria": "複製完整提交雜湊 {hash}",
"pushStatus": {
"pushed": "已推送到遠端",
"notPushed": "未推送到遠端",
"unknown": "推送狀態未知(未配置上游分支)"
},
"time": {
"monthsAgo": "{count} 個月前",
"daysAgo": "{count} 天前",
"hoursAgo": "{count} 小時前",
"minsAgo": "{count} 分鐘前",
"justNow": "剛剛"
},
"toasts": {
"createdAndSwitchedNewBranch": "已建立並切換到新分支",
"newBranchFromCommit": "{name}(來自 {shortHash}",
"createBranchFailed": "新增分支失敗"
},
"branchSelector": {
"selectBranchPlaceholder": "選擇分支...",
"localBranches": "本地分支",
"current": "目前",
"remoteBranches": "遠端分支",
"refreshCommitHistory": "刷新提交記錄"
},
"dialogs": {
"newBranchTitle": "新增分支",
"newBranchDescription": "以提交 {shortHash} 作為最後提交建立新分支。",
"branchNamePlaceholder": "分支名稱"
}
},
"gitChangesTab": {
"workspace": "工作區",
"noChanges": "暫無本地變更",
"trackedChanges": "本地已追蹤變更 ({count})",
"untrackedFiles": "本地未追蹤檔案 ({count})",
"expandTracked": "展開已追蹤變更",
"collapseTracked": "折疊已追蹤變更",
"expandUntracked": "展開未追蹤檔案",
"collapseUntracked": "折疊未追蹤檔案",
"actions": {
"rollback": "回滾",
"addToVcs": "加入到 VCS"
},
"toasts": {
"noAddableFilesInDir": "該目錄下沒有可加入到 VCS 的變更檔案",
"noRollbackFilesInDir": "該目錄下沒有可回滾的變更檔案",
"addedToVcs": "已加入 {name} 到 VCS",
"addToVcsFailed": "加入到 VCS 失敗",
"rolledBack": "已回滾 {name}",
"rollbackFailed": "回滾失敗",
"addedFilesToVcs": "已加入 {count} 個檔案到 VCS",
"rolledBackFiles": "已回滾 {count} 個檔案"
},
"directoryDialog": {
"descriptionAdd": "選擇目錄 {path} 下要加入到 VCS 的檔案。",
"descriptionRollback": "選擇目錄 {path} 下要回滾的檔案。",
"descriptionFallback": "選擇要操作的檔案。",
"selectionCount": "已選擇 {selected} / {total} 個檔案",
"selectAll": "全選",
"unselectAll": "取消全選",
"loadingCandidates": "正在載入目錄變更...",
"noOperableFiles": "沒有可操作的檔案"
},
"rollbackConfirm": {
"title": "確認回滾",
"descriptionWithTarget": "確定回滾{kind}「{name}」的本地修改嗎?",
"descriptionFallback": "確定回滾本地修改嗎?",
"kindDirectory": "目錄",
"kindFile": "檔案"
}
},
"fileTreeTab": {
"workspace": "工作區",
"retry": "重試",
"git": "Git",
"openInFileManager": "在檔案管理器開啟",
"openInFinder": "在 Finder 開啟",
"openInExplorer": "在檔案總管開啟",
"attachToCurrentSession": "附加到目前會話",
"compareWithBranch": "與分支比較...",
"reloadFromDisk": "從磁碟重新載入",
"openIn": "開啟於",
"openInTerminal": "在終端開啟",
"actions": {
"select": "選擇",
"unselect": "取消選擇",
"rollback": "回滾",
"addToVcs": "加入到 VCS"
},
"aria": {
"selectPath": "{action} {path}"
},
"toasts": {
"openDirectoryFailed": "開啟目錄失敗",
"openBuiltinTerminalFailed": "無法開啟內建終端",
"noAddableFilesInDir": "該目錄下沒有可加入到 VCS 的變更檔案",
"noRollbackFilesInDir": "該目錄下沒有可回滾的變更檔案",
"addedToVcs": "已加入 {name} 到 VCS",
"addToVcsFailed": "加入到 VCS 失敗",
"loadBranchesFailed": "載入分支失敗",
"renameFailed": "重新命名失敗",
"deleteFailed": "刪除失敗",
"rolledBack": "已回滾 {name}",
"rollbackFailed": "回滾失敗",
"addedFilesToVcs": "已加入 {count} 個檔案到 VCS",
"rolledBackFiles": "已回滾 {count} 個檔案",
"savedAsCopy": "已另存為副本",
"saveCopyFailed": "另存為副本失敗",
"watchStartFailed": "檔案監聽啟動失敗"
},
"renameDialog": {
"renameDirectory": "重新命名目錄",
"renameFile": "重新命名檔案",
"description": "輸入新的名稱(僅名稱,不含路徑)。",
"placeholderDirectory": "new-folder-name",
"placeholderFile": "new-file-name.ext"
},
"directoryDialog": {
"descriptionAdd": "選擇目錄 {path} 下要加入到 VCS 的檔案。",
"descriptionRollback": "選擇目錄 {path} 下要回滾的檔案。",
"descriptionFallback": "選擇要操作的檔案。",
"selectionCount": "已選擇 {selected} / {total} 個檔案",
"selectAll": "全選",
"unselectAll": "取消全選",
"loadingCandidates": "正在載入目錄變更...",
"noOperableFiles": "沒有可操作的檔案"
},
"compareDialog": {
"title": "與分支比較",
"descriptionWithTarget": "選擇分支並與{kind} {path} 比對",
"descriptionFallback": "選擇要比較的分支。",
"kindDirectory": "目錄",
"kindFile": "檔案",
"filterPlaceholder": "過濾分支,例如 main / origin/main",
"singleClickHint": "單擊分支即可直接比較",
"loadingBranches": "正在載入分支...",
"recentBranches": "最近分支 ({count})",
"noCurrentBranch": "無目前分支",
"localBranches": "本地分支 ({count})",
"remoteBranches": "遠端分支 ({count})",
"noMatchingBranches": "無匹配分支"
},
"externalConflictDialog": {
"title": "偵測到外部檔案變更",
"descriptionWithPath": "檔案 {path} 在磁碟已發生變化,目前編輯內容尚未儲存。",
"descriptionFallback": "目前檔案在磁碟已發生變化,目前編輯內容尚未儲存。",
"compare": "比較",
"savingCopy": "另存中...",
"saveAsCopy": "另存為副本",
"reload": "重載"
},
"deleteConfirm": {
"title": "確認刪除",
"descriptionWithTarget": "確定刪除{kind} \"{name}\" 嗎?此操作不可撤銷。",
"descriptionFallback": "此操作不可撤銷。",
"kindDirectory": "目錄",
"kindFile": "檔案"
},
"rollbackConfirm": {
"title": "確認回滾",
"descriptionWithTarget": "確定回滾檔案 \"{name}\" 的本地修改嗎?",
"descriptionFallback": "確定回滾該檔案的本地修改嗎?"
},
"terminalTitle": "終端 · {name}"
},
"commandDropdown": {
"loading": "載入中...",
"addCommand": "新增命令",
"manageCommands": "管理命令...",
"runCommandTitle": "執行:{command}",
"stopCommandTitle": "停止:{command}",
"manageDialog": {
"title": "管理命令",
"empty": "暫無命令",
"namePlaceholder": "名稱",
"commandPlaceholder": "命令",
"add": "新增",
"saving": "儲存中..."
}
}
}
}