设置窗口多语言处理

This commit is contained in:
xintaofei
2026-03-07 11:07:06 +08:00
parent 28babff52c
commit 5ca9fd0b2e
8 changed files with 1861 additions and 453 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import {
ShieldCheck,
TerminalSquare,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -62,6 +63,11 @@ type Selection =
| { kind: "market"; id: string }
| null
type McpTranslator = (
key: string,
values?: Record<string, string | number>
) => string
const APP_OPTIONS: { value: McpAppType; label: string }[] = [
{ value: "claude_code", label: "Claude" },
{ value: "codex", label: "Codex" },
@@ -79,11 +85,11 @@ function readString(spec: Record<string, unknown>, key: string): string | null {
return trimmed ? trimmed : null
}
function specSummary(spec: Record<string, unknown>): string {
function specSummary(spec: Record<string, unknown>, t: McpTranslator): string {
const typ = readString(spec, "type") ?? "stdio"
if (typ === "stdio") {
const command = readString(spec, "command") ?? "(missing command)"
const command = readString(spec, "command") ?? t("summary.missingCommand")
const rawArgs = spec.args
const args = Array.isArray(rawArgs)
? rawArgs
@@ -93,12 +99,12 @@ function specSummary(spec: Record<string, unknown>): string {
return args.length > 0 ? `${command} ${args.join(" ")}` : command
}
const url = readString(spec, "url") ?? "(missing url)"
const url = readString(spec, "url") ?? t("summary.missingUrl")
return `${typ}: ${url}`
}
function protocolBadgeLabel(protocol: string): string {
if (protocol === "stdio") return "Stdio"
function protocolBadgeLabel(protocol: string, t: McpTranslator): string {
if (protocol === "stdio") return t("protocol.stdio")
if (protocol === "sse") return "SSE"
if (protocol === "http") return "HTTP"
return protocol
@@ -130,9 +136,10 @@ function defaultParamDraft(
function parseParameterValues(
option: McpMarketplaceInstallOption | null,
draft: Record<string, string>
draft: Record<string, string>,
t: McpTranslator
): { values: Record<string, unknown>; error: string | null } {
if (!option) return { values: {}, error: "请选择安装协议" }
if (!option) return { values: {}, error: t("errors.selectInstallProtocol") }
const values: Record<string, unknown> = {}
for (const field of option.parameters) {
@@ -140,7 +147,10 @@ function parseParameterValues(
if (!raw) {
if (field.required && field.default_value == null) {
return { values: {}, error: `${field.label} 为必填项` }
return {
values: {},
error: t("errors.fieldRequired", { field: field.label }),
}
}
continue
}
@@ -149,7 +159,7 @@ function parseParameterValues(
if (raw !== "true" && raw !== "false") {
return {
values: {},
error: `${field.label} 需要 true 或 false`,
error: t("errors.fieldNeedsBoolean", { field: field.label }),
}
}
values[field.key] = raw === "true"
@@ -159,7 +169,10 @@ function parseParameterValues(
if (field.kind === "number") {
const next = Number(raw)
if (!Number.isFinite(next)) {
return { values: {}, error: `${field.label} 需要数字` }
return {
values: {},
error: t("errors.fieldNeedsNumber", { field: field.label }),
}
}
values[field.key] = next
continue
@@ -168,7 +181,10 @@ function parseParameterValues(
if (field.kind === "integer") {
const next = Number(raw)
if (!Number.isInteger(next)) {
return { values: {}, error: `${field.label} 需要整数` }
return {
values: {},
error: t("errors.fieldNeedsInteger", { field: field.label }),
}
}
values[field.key] = next
continue
@@ -179,13 +195,22 @@ function parseParameterValues(
values[field.key] = JSON.parse(raw)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return { values: {}, error: `${field.label} JSON 无效:${message}` }
return {
values: {},
error: t("errors.fieldInvalidJson", {
field: field.label,
message,
}),
}
}
continue
}
if (field.enum_values.length > 0 && !field.enum_values.includes(raw)) {
return { values: {}, error: `${field.label} 的值不在可选范围内` }
return {
values: {},
error: t("errors.fieldOutOfRange", { field: field.label }),
}
}
values[field.key] = raw
@@ -215,10 +240,14 @@ function selectedAppsFromDraft(
)
}
function parseJsonObject(text: string, name: string): Record<string, unknown> {
function parseJsonObject(
text: string,
name: string,
t: McpTranslator
): Record<string, unknown> {
const trimmed = text.trim()
if (!trimmed) {
throw new Error(`${name} 不能为空`)
throw new Error(t("errors.jsonEmpty", { name }))
}
let parsed: unknown
@@ -226,17 +255,19 @@ function parseJsonObject(text: string, name: string): Record<string, unknown> {
parsed = JSON.parse(trimmed)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
throw new Error(`${name} 不是合法 JSON${message}`)
throw new Error(t("errors.jsonInvalid", { name, message }))
}
if (!isObject(parsed)) {
throw new Error(`${name} 必须是 JSON 对象`)
throw new Error(t("errors.jsonMustBeObject", { name }))
}
return parsed
}
export function McpSettings() {
const t = useTranslations("McpSettings")
const mcpT = t as unknown as McpTranslator
const [loading, setLoading] = useState(true)
const [loadingError, setLoadingError] = useState<string | null>(null)
@@ -306,9 +337,9 @@ export function McpSettings() {
return installedServers.filter((item) => {
if (item.id.toLowerCase().includes(q)) return true
const spec = isObject(item.spec) ? item.spec : {}
return specSummary(spec).toLowerCase().includes(q)
return specSummary(spec, mcpT).toLowerCase().includes(q)
})
}, [installedServers, localFilter])
}, [installedServers, localFilter, mcpT])
const refreshLocalServers = useCallback(async () => {
const servers = await mcpScanLocal()
@@ -464,7 +495,7 @@ export function McpSettings() {
try {
await mcpRemoveServer(serverId)
const next = await refreshLocalServers()
toast.success("已卸载 MCP")
toast.success(t("toasts.uninstalled"))
setSelection((current) => {
if (current?.kind !== "local" || current.id !== serverId)
@@ -474,12 +505,12 @@ export function McpSettings() {
})
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(`卸载失败:${message}`)
toast.error(t("toasts.uninstallFailed", { message }))
} finally {
setRunningAction(null)
}
},
[refreshLocalServers]
[refreshLocalServers, t]
)
const saveLocalServer = useCallback(async () => {
@@ -487,7 +518,11 @@ export function McpSettings() {
let parsedSpec: Record<string, unknown>
try {
parsedSpec = parseJsonObject(localSpecText, "MCP 配置")
parsedSpec = parseJsonObject(
localSpecText,
t("jsonNames.localConfig"),
mcpT
)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(message)
@@ -496,7 +531,7 @@ export function McpSettings() {
const apps = normalizeApps(selectedAppsFromDraft(localAppsDraft))
if (apps.length === 0) {
toast.error("请至少选择一个目标应用")
toast.error(t("toasts.selectAtLeastOneApp"))
return
}
@@ -510,7 +545,7 @@ export function McpSettings() {
apps,
})
const next = await refreshLocalServers()
toast.success("保存成功")
toast.success(t("toasts.saveSuccess"))
const updated = next.find((item) => item.id === selectedLocal.id)
if (updated) {
@@ -520,11 +555,11 @@ export function McpSettings() {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(`保存失败:${message}`)
toast.error(t("toasts.saveFailed", { message }))
} finally {
setRunningAction(null)
}
}, [localAppsDraft, localSpecText, refreshLocalServers, selectedLocal])
}, [localAppsDraft, localSpecText, refreshLocalServers, selectedLocal, t])
const switchInstallOption = useCallback(
(optionId: string) => {
@@ -562,7 +597,8 @@ export function McpSettings() {
const parsedParams = parseParameterValues(
selectedInstallOption,
installParamDraft
installParamDraft,
mcpT
)
if (parsedParams.error) {
toast.error(parsedParams.error)
@@ -578,7 +614,11 @@ export function McpSettings() {
const currentSpecText = marketSpecText.trim()
if (marketSpecDirty && currentSpecText !== baselineText) {
try {
specOverride = parseJsonObject(marketSpecText, "安装配置")
specOverride = parseJsonObject(
marketSpecText,
t("jsonNames.installConfig"),
mcpT
)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(message)
@@ -588,7 +628,7 @@ export function McpSettings() {
const apps = normalizeApps(selectedAppsFromDraft(installAppsDraft))
if (apps.length === 0) {
toast.error("请至少选择一个目标应用")
toast.error(t("toasts.selectAtLeastOneApp"))
return
}
@@ -606,7 +646,7 @@ export function McpSettings() {
specOverride,
})
const nextLocal = await refreshLocalServers()
toast.success(`已安装 ${marketDetail.name}`)
toast.success(t("toasts.installed", { name: marketDetail.name }))
setInstallDialogOpen(false)
setLeftTab("local")
@@ -618,7 +658,7 @@ export function McpSettings() {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(`安装失败:${message}`)
toast.error(t("toasts.installFailed", { message }))
} finally {
setRunningAction(null)
}
@@ -630,13 +670,14 @@ export function McpSettings() {
marketSpecText,
refreshLocalServers,
selectedInstallOption,
t,
])
if (loading) {
return (
<div className="h-full flex items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
{t("loading")}
</div>
)
}
@@ -646,28 +687,35 @@ export function McpSettings() {
<Dialog open={installDialogOpen} onOpenChange={setInstallDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle> MCP</DialogTitle>
<DialogTitle>{t("installDialog.title")}</DialogTitle>
<DialogDescription>
{marketDetail
? `${marketDetail.name} 安装到本地配置。`
: "选择安装目标应用。"}
? t("installDialog.descriptionWithName", {
name: marketDetail.name,
})
: t("installDialog.description")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 text-sm">
<div className="space-y-2">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">
{t("installDialog.protocol")}
</div>
<Select
value={selectedInstallOption?.id ?? ""}
onValueChange={switchInstallOption}
>
<SelectTrigger>
<SelectValue placeholder="选择协议" />
<SelectValue
placeholder={t("installDialog.selectProtocol")}
/>
</SelectTrigger>
<SelectContent>
{(marketDetail?.install_options ?? []).map((option) => (
<SelectItem key={option.id} value={option.id}>
{protocolBadgeLabel(option.protocol)} · {option.label}
{protocolBadgeLabel(option.protocol, mcpT)} ·{" "}
{option.label}
</SelectItem>
))}
</SelectContent>
@@ -676,7 +724,9 @@ export function McpSettings() {
{selectedInstallOption?.parameters.length ? (
<div className="space-y-2">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">
{t("installDialog.parameters")}
</div>
<div className="max-h-56 overflow-auto space-y-2 pr-1">
{selectedInstallOption.parameters.map((field) => {
const raw = installParamDraft[field.key] ?? ""
@@ -704,7 +754,11 @@ export function McpSettings() {
}
>
<SelectTrigger>
<SelectValue placeholder="请选择 true/false" />
<SelectValue
placeholder={t(
"installDialog.booleanPlaceholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="true">true</SelectItem>
@@ -722,7 +776,9 @@ export function McpSettings() {
}
>
<SelectTrigger>
<SelectValue placeholder="选择一个值" />
<SelectValue
placeholder={t("installDialog.selectOneValue")}
/>
</SelectTrigger>
<SelectContent>
{field.enum_values.map((value) => (
@@ -770,7 +826,9 @@ export function McpSettings() {
) : null}
<div className="space-y-2">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">
{t("installDialog.targetApps")}
</div>
{APP_OPTIONS.map((app) => (
<label
key={app.value}
@@ -798,7 +856,7 @@ export function McpSettings() {
onClick={() => setInstallDialogOpen(false)}
disabled={Boolean(runningAction?.startsWith("install:"))}
>
{t("actions.cancel")}
</Button>
<Button
onClick={() => {
@@ -811,10 +869,10 @@ export function McpSettings() {
{runningAction?.startsWith("install:") ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("actions.installing")}
</>
) : (
"确认安装"
t("actions.confirmInstall")
)}
</Button>
</DialogFooter>
@@ -830,10 +888,10 @@ export function McpSettings() {
>
<TabsList className="w-full">
<TabsTrigger value="local" className="flex-1">
MCP
{t("tabs.local")}
</TabsTrigger>
<TabsTrigger value="market" className="flex-1">
MCP
{t("tabs.market")}
</TabsTrigger>
</TabsList>
@@ -842,7 +900,7 @@ export function McpSettings() {
<Input
value={localFilter}
onChange={(event) => setLocalFilter(event.target.value)}
placeholder="筛选本地 MCP..."
placeholder={t("local.filterPlaceholder")}
/>
<Button
size="icon"
@@ -859,14 +917,14 @@ export function McpSettings() {
{loadingError ? (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{loadingError}
{t("local.loadFailed", { message: loadingError })}
</div>
) : null}
<div className="h-[calc(100%-48px)] overflow-auto space-y-1">
{filteredLocalServers.length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-xs text-muted-foreground">
MCP
{t("local.empty")}
</div>
) : (
filteredLocalServers.map((server) => {
@@ -891,7 +949,7 @@ export function McpSettings() {
{server.id}
</div>
<div className="text-xs text-muted-foreground line-clamp-2 break-all">
{specSummary(spec)}
{specSummary(spec, mcpT)}
</div>
</button>
</ContextMenuTrigger>
@@ -907,7 +965,7 @@ export function McpSettings() {
})
}}
>
{t("actions.uninstall")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -924,7 +982,7 @@ export function McpSettings() {
onValueChange={setSelectedProvider}
>
<SelectTrigger>
<SelectValue placeholder="选择市场" />
<SelectValue placeholder={t("market.selectMarketplace")} />
</SelectTrigger>
<SelectContent>
{providers.map((provider) => (
@@ -939,7 +997,7 @@ export function McpSettings() {
<Input
value={marketQuery}
onChange={(event) => setMarketQuery(event.target.value)}
placeholder="搜索 MCP..."
placeholder={t("market.searchPlaceholder")}
onKeyDown={(event) => {
if (event.key !== "Enter") return
executeSearch({
@@ -978,7 +1036,7 @@ export function McpSettings() {
{searchError ? (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{searchError}
{t("market.searchFailed", { message: searchError })}
</div>
) : null}
@@ -986,11 +1044,11 @@ export function McpSettings() {
{searching ? (
<div className="h-full min-h-24 rounded-md border border-dashed flex items-center justify-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
MCP ...
{t("market.loadingList")}
</div>
) : searchResults.length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-xs text-muted-foreground">
MCP
{t("market.empty")}
</div>
) : (
searchResults.map((item) => {
@@ -1048,7 +1106,7 @@ export function McpSettings() {
variant="secondary"
className="text-[10px]"
>
{protocolBadgeLabel(protocol)}
{protocolBadgeLabel(protocol, mcpT)}
</Badge>
))}
{item.latest_version ? (
@@ -1060,14 +1118,16 @@ export function McpSettings() {
</Badge>
) : null}
{item.verified ? (
<Badge className="text-[10px]">Verified</Badge>
<Badge className="text-[10px]">
{t("badges.verified")}
</Badge>
) : null}
{typeof item.downloads === "number" ? (
<Badge
variant="outline"
className="text-[10px]"
>
{item.downloads} uses
{t("badges.uses", { count: item.downloads })}
</Badge>
) : null}
</div>
@@ -1082,7 +1142,7 @@ export function McpSettings() {
})
}}
>
{t("actions.viewDetails")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -1103,7 +1163,7 @@ export function McpSettings() {
{selectedLocal.id}
</h2>
<p className="text-xs text-muted-foreground mt-1">
MCP
{t("local.description")}
</p>
</div>
<Button
@@ -1118,16 +1178,18 @@ export function McpSettings() {
{runningAction === `uninstall:${selectedLocal.id}` ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("actions.uninstalling")}
</>
) : (
"卸载"
t("actions.uninstall")
)}
</Button>
</div>
<div className="space-y-2">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">
{t("local.enabledApps")}
</div>
<div className="flex flex-wrap gap-2">
{APP_OPTIONS.map((app) => (
<label
@@ -1152,7 +1214,7 @@ export function McpSettings() {
<div className="space-y-2">
<div className="text-xs text-muted-foreground">
MCP (JSON)
{t("local.configJson")}
</div>
<Textarea
value={localSpecText}
@@ -1173,10 +1235,10 @@ export function McpSettings() {
{runningAction === `save:${selectedLocal.id}` ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("actions.saving")}
</>
) : (
"保存"
t("actions.save")
)}
</Button>
</div>
@@ -1188,11 +1250,11 @@ export function McpSettings() {
{marketDetailLoading ? (
<div className="h-40 flex items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
{t("market.loadingDetail")}
</div>
) : marketDetailError ? (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{marketDetailError}
{t("market.detailLoadFailed", { message: marketDetailError })}
</div>
) : marketDetail ? (
<>
@@ -1221,20 +1283,24 @@ export function McpSettings() {
</p>
</div>
</div>
<Button onClick={openInstallDialog}></Button>
<Button onClick={openInstallDialog}>
{t("actions.install")}
</Button>
</div>
<div className="flex flex-wrap gap-1.5">
{marketDetail.verified ? <Badge>Verified</Badge> : null}
{marketDetail.verified ? (
<Badge>{t("badges.verified")}</Badge>
) : null}
{marketDetail.remote ? (
<Badge variant="secondary">Remote</Badge>
<Badge variant="secondary">{t("badges.remote")}</Badge>
) : null}
{marketDetail.homepage ? (
<Badge variant="outline">Has Homepage</Badge>
<Badge variant="outline">{t("badges.hasHomepage")}</Badge>
) : null}
{marketDetail.protocols.map((protocol) => (
<Badge key={`detail-${protocol}`} variant="secondary">
{protocolBadgeLabel(protocol)}
{protocolBadgeLabel(protocol, mcpT)}
</Badge>
))}
{marketDetail.latest_version ? (
@@ -1244,7 +1310,7 @@ export function McpSettings() {
) : null}
{typeof marketDetail.downloads === "number" ? (
<Badge variant="outline">
{marketDetail.downloads} uses
{t("badges.uses", { count: marketDetail.downloads })}
</Badge>
) : null}
</div>
@@ -1268,52 +1334,59 @@ export function McpSettings() {
{marketDetail.owner ? (
<div className="inline-flex items-center gap-1.5">
<ShieldCheck className="h-3.5 w-3.5" />
Owner: {marketDetail.owner}
{t("market.owner", { owner: marketDetail.owner })}
</div>
) : null}
{marketDetail.namespace ? (
<div className="inline-flex items-center gap-1.5">
<TerminalSquare className="h-3.5 w-3.5" />
Namespace: {marketDetail.namespace}
{t("market.namespace", {
namespace: marketDetail.namespace,
})}
</div>
) : null}
{marketDetail.is_deployed != null ? (
<div className="inline-flex items-center gap-1.5">
<Globe className="h-3.5 w-3.5" />
{marketDetail.is_deployed ? "Deployed" : "Not Deployed"}
{marketDetail.is_deployed
? t("badges.deployed")
: t("badges.notDeployed")}
</div>
) : null}
</div>
<div className="space-y-2">
<div className="text-xs text-muted-foreground">
{t("market.defaultInstallProtocol")}
</div>
<Select
value={selectedInstallOption?.id ?? ""}
onValueChange={switchInstallOption}
>
<SelectTrigger>
<SelectValue placeholder="选择协议" />
<SelectValue
placeholder={t("installDialog.selectProtocol")}
/>
</SelectTrigger>
<SelectContent>
{marketDetail.install_options.map((option) => (
<SelectItem key={option.id} value={option.id}>
{protocolBadgeLabel(option.protocol)} ·{" "}
{protocolBadgeLabel(option.protocol, mcpT)} ·{" "}
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-[11px] text-muted-foreground">
{selectedInstallOption?.parameters.length ?? 0}
{t("market.currentOptionParameterCount", {
count: selectedInstallOption?.parameters.length ?? 0,
})}
</div>
</div>
<div className="space-y-2">
<div className="text-xs text-muted-foreground">
(JSON/)
{t("market.installConfigDescription")}
</div>
<Textarea
value={marketSpecText}
@@ -1327,7 +1400,7 @@ export function McpSettings() {
</>
) : (
<div className="rounded-md border border-dashed p-3 text-xs text-muted-foreground">
MCP
{t("market.selectLeftToView")}
</div>
)}
</div>
@@ -1335,7 +1408,7 @@ export function McpSettings() {
{!selection ? (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
MCP
{t("selectLeftMcp")}
</div>
) : null}
</section>

View File

@@ -1,7 +1,8 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { Keyboard, RotateCcw } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { useIsMac } from "@/hooks/use-is-mac"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
@@ -27,10 +28,19 @@ function canShareShortcut(a: ShortcutActionId, b: ShortcutActionId): boolean {
}
export function ShortcutSettings() {
const t = useTranslations("ShortcutSettings")
const { shortcuts, updateShortcut, resetShortcuts } = useShortcutSettings()
const isMac = useIsMac()
const [recordingAction, setRecordingAction] =
useState<ShortcutActionId | null>(null)
const actionTitle = useCallback(
(id: ShortcutActionId) => t(`actions.${id}.title`),
[t]
)
const actionDescription = useCallback(
(id: ShortcutActionId) => t(`actions.${id}.description`),
[t]
)
const isDefault = useMemo(
() =>
@@ -65,14 +75,14 @@ export function ShortcutSettings() {
)
if (conflict) {
toast.error(`快捷键已被「${conflict.title}」占用`)
toast.error(t("toasts.conflict", { title: actionTitle(conflict.id) }))
return
}
if (updateShortcut(recordingAction, shortcut)) {
toast.success("快捷键已更新")
toast.success(t("toasts.updated"))
} else {
toast.error("快捷键无效,请重试")
toast.error(t("toasts.invalid"))
}
setRecordingAction(null)
@@ -83,7 +93,7 @@ export function ShortcutSettings() {
return () => {
window.removeEventListener("keydown", onKeyDown, true)
}
}, [recordingAction, shortcuts, updateShortcut])
}, [actionTitle, recordingAction, shortcuts, t, updateShortcut])
return (
<div className="h-full overflow-auto">
@@ -92,7 +102,7 @@ export function ShortcutSettings() {
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Keyboard className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold"></h2>
<h2 className="text-sm font-semibold">{t("sectionTitle")}</h2>
</div>
<Button
variant="outline"
@@ -100,18 +110,17 @@ export function ShortcutSettings() {
onClick={() => {
resetShortcuts()
setRecordingAction(null)
toast.success("已恢复默认快捷键")
toast.success(t("toasts.reset"))
}}
disabled={isDefault}
>
<RotateCcw className="h-3.5 w-3.5" />
{t("resetDefault")}
</Button>
</div>
<p className="text-xs text-muted-foreground leading-5">
使 Ctrl/CmdAltShift
Esc
{t("recordInstruction")}
</p>
<div className="space-y-2">
@@ -125,10 +134,10 @@ export function ShortcutSettings() {
>
<div className="min-w-0">
<div className="text-sm font-medium">
{definition.title}
{actionTitle(definition.id)}
</div>
<p className="text-xs text-muted-foreground truncate">
{definition.description}
{actionDescription(definition.id)}
</p>
</div>
<Button
@@ -142,7 +151,7 @@ export function ShortcutSettings() {
}}
>
{isRecording
? "按下快捷键..."
? t("recording")
: formatShortcutLabel(shortcuts[definition.id], isMac)}
</Button>
</div>

View File

@@ -11,6 +11,7 @@ import {
RotateCcw,
Save,
} from "lucide-react"
import { useTranslations } from "next-intl"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { toast } from "sonner"
@@ -63,71 +64,28 @@ import type {
AgentType,
} from "@/lib/types"
function defaultSkillContent(agentType: AgentType): string {
type SkillsTranslator = (
key: string,
values?: Record<string, string | number>
) => string
function defaultSkillContent(
agentType: AgentType,
t: SkillsTranslator
): string {
if (agentType === "gemini") {
return `---
name: example-skill
description: Describe when this skill should be used.
---
# Skill Name
Instructions for the agent when this skill is active.
## Workflow
1. Add actionable step one.
2. Add actionable step two.
`
return t("templates.gemini")
}
if (agentType === "open_code") {
return `---
name: example-skill
description: Describe when this skill should be used.
---
# Purpose
Describe what this skill helps with.
# Steps
1. Add actionable step one.
2. Add actionable step two.
`
return t("templates.openCode")
}
if (agentType === "open_claw") {
return `---
name: example-skill
description: Describe when this skill should be used.
user-invocable: true
disable-model-invocation: false
---
# Purpose
Describe what this skill helps with.
# Instructions
1. Add actionable instruction one.
2. Add actionable instruction two.
`
return t("templates.openClaw")
}
return `# Skill: example-skill
## When to use
- Describe trigger conditions.
## Instructions
1. Add actionable instruction one.
2. Add actionable instruction two.
`
return t("templates.default")
}
function defaultSkillLayoutForAgent(
@@ -245,6 +203,8 @@ function parseYamlFrontMatter(content: string): ParsedFrontMatter {
}
export function SkillsSettings() {
const t = useTranslations("SkillsSettings")
const skillsT = t as unknown as SkillsTranslator
const panelContainerRef = useRef<HTMLDivElement | null>(null)
const [panelContainerWidth, setPanelContainerWidth] = useState(0)
const [agents, setAgents] = useState<AcpAgentInfo[]>([])
@@ -349,10 +309,10 @@ export function SkillsSettings() {
(agentType: AgentType, contentEditing = false) => {
setSelectedSkillId(null)
setSkillDraftId("")
setSkillDraftContent(defaultSkillContent(agentType))
setSkillDraftContent(defaultSkillContent(agentType, skillsT))
setIsContentEditing(contentEditing)
},
[]
[skillsT]
)
const openSkill = useCallback(
@@ -374,12 +334,12 @@ export function SkillsSettings() {
setIsContentEditing(mode === "edit")
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error("加载 Skill 失败", { description: message })
toast.error(t("toasts.loadFailed"), { description: message })
} finally {
setSkillReading(false)
}
},
[]
[t]
)
const loadSkills = useCallback(async (agentType: AgentType) => {
@@ -466,10 +426,10 @@ export function SkillsSettings() {
await openFolderWindow(dirPath)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error("打开目录失败", { description: message })
toast.error(t("toasts.openFolderFailed"), { description: message })
}
},
[]
[t]
)
const handleRequestDeleteSkill = useCallback((skill: AgentSkillItem) => {
@@ -502,13 +462,13 @@ export function SkillsSettings() {
const handleSaveSkill = useCallback(async () => {
if (!selectedAgent) return
if (!skillLocation) {
toast.error("当前 Agent 未找到可用的 Skills 目录")
toast.error(t("toasts.noSkillDirectory"))
return
}
const trimmedId = skillDraftId.trim()
if (!trimmedId) {
toast.error("Skill 名称不能为空")
toast.error(t("toasts.nameRequired"))
return
}
@@ -528,10 +488,12 @@ export function SkillsSettings() {
saved,
isContentEditing ? "edit" : "preview"
)
toast.success(isEditingExisting ? "Skill 已更新" : "Skill 已创建")
toast.success(
isEditingExisting ? t("toasts.updated") : t("toasts.created")
)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error("保存 Skill 失败", { description: message })
toast.error(t("toasts.saveFailed"), { description: message })
} finally {
setSkillSaving(false)
}
@@ -545,6 +507,7 @@ export function SkillsSettings() {
skillDraftId,
skillLocation,
isContentEditing,
t,
])
const handleDeleteSkill = useCallback(
@@ -562,7 +525,7 @@ export function SkillsSettings() {
})
const latest = await loadSkills(selectedAgent.agent_type)
toast.success("Skill 已删除")
toast.success(t("toasts.deleted"))
if (!deletingCurrent) return
@@ -574,14 +537,14 @@ export function SkillsSettings() {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error("删除 Skill 失败", { description: message })
toast.error(t("toasts.deleteFailed"), { description: message })
} finally {
setSkillDeletingId(null)
setDeleteDialogOpen(false)
setDeleteTargetSkill(null)
}
},
[loadSkills, openSkill, resetDraft, selectedAgent, selectedSkillId]
[loadSkills, openSkill, resetDraft, selectedAgent, selectedSkillId, t]
)
const handleConfirmDelete = useCallback(async () => {
@@ -692,7 +655,7 @@ export function SkillsSettings() {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Skills Agent...
{t("loadingAgents")}
</div>
)
}
@@ -701,9 +664,9 @@ export function SkillsSettings() {
<div className="h-full flex flex-col">
<div className="flex items-center justify-between gap-3 pb-4">
<div>
<h2 className="text-base font-semibold">Skills</h2>
<h2 className="text-base font-semibold">{t("title")}</h2>
<p className="text-xs text-muted-foreground mt-1">
Skill Markdown
{t("description")}
</p>
</div>
</div>
@@ -716,7 +679,7 @@ export function SkillsSettings() {
{sortedAgents.length === 0 ? (
<div className="h-full rounded-lg border bg-card flex items-center justify-center text-sm text-muted-foreground">
Skills Agent
{t("emptyNoManageableAgents")}
</div>
) : (
<div ref={panelContainerRef} className="flex-1 min-h-0 min-w-0">
@@ -732,7 +695,7 @@ export function SkillsSettings() {
<div className="min-h-0 h-full min-w-0 rounded-lg border bg-card flex flex-col overflow-hidden lg:rounded-r-none">
<div className="border-b p-3 space-y-2.5">
<div className="text-xs font-medium text-muted-foreground">
{t("managedTarget")}
</div>
<Select
value={selectedAgentType ?? ""}
@@ -741,7 +704,7 @@ export function SkillsSettings() {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择 Agent" />
<SelectValue placeholder={t("selectAgentPlaceholder")} />
</SelectTrigger>
<SelectContent align="start">
{sortedAgents.map((agent) => (
@@ -760,12 +723,12 @@ export function SkillsSettings() {
onChange={(event) => {
setSearchQuery(event.target.value)
}}
placeholder="搜索名称 / ID / 路径..."
placeholder={t("searchPlaceholder")}
/>
</div>
<div className="border-b px-3 py-2 text-xs font-medium text-muted-foreground flex items-center justify-between gap-2">
<span>Skills </span>
<span>{t("skillsList")}</span>
<span>{filteredSkills.length}</span>
</div>
@@ -773,7 +736,7 @@ export function SkillsSettings() {
{skillsLoading && (
<div className="text-xs text-muted-foreground flex items-center gap-1.5 p-1">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Skills ...
{t("loadingSkills")}
</div>
)}
@@ -785,7 +748,7 @@ export function SkillsSettings() {
{!skillsLoading && !skillsError && !skillsSupported && (
<div className="text-xs text-muted-foreground rounded-md border bg-muted/20 px-2.5 py-2">
Agent Skills
{t("agentNotSupported")}
</div>
)}
@@ -793,7 +756,7 @@ export function SkillsSettings() {
skillsSupported &&
filteredSkills.length === 0 && (
<div className="text-xs text-muted-foreground px-1">
Skill Skill
{t("emptySkills")}
</div>
)}
@@ -850,7 +813,7 @@ export function SkillsSettings() {
})
}}
>
{t("actions.preview")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -862,7 +825,7 @@ export function SkillsSettings() {
})
}}
>
{t("actions.edit")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
@@ -874,7 +837,7 @@ export function SkillsSettings() {
})
}}
>
{t("actions.openInWindow")}
</ContextMenuItem>
<ContextMenuItem
disabled={skillSaving || skillReading || deleting}
@@ -883,7 +846,9 @@ export function SkillsSettings() {
}}
className="text-destructive focus:text-destructive"
>
{deleting ? "删除中..." : "删除"}
{deleting
? t("actions.deleting")
: t("actions.delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -912,7 +877,7 @@ export function SkillsSettings() {
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("actions.refresh")}
</Button>
<Button
size="sm"
@@ -921,7 +886,7 @@ export function SkillsSettings() {
disabled={!selectedAgent}
>
<Plus className="h-3.5 w-3.5" />
Skill
{t("actions.newSkill")}
</Button>
</div>
</div>
@@ -936,7 +901,7 @@ export function SkillsSettings() {
<div className="border-b px-4 py-3 flex items-center justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold truncate">
{skillDraftId.trim() || "新建 Skill"}
{skillDraftId.trim() || t("newSkillTitle")}
</h3>
</div>
@@ -948,7 +913,7 @@ export function SkillsSettings() {
disabled={skillSaving || skillReading}
>
<RotateCcw className="h-3 w-3" />
{t("actions.reset")}
</Button>
<Button
size="xs"
@@ -965,12 +930,12 @@ export function SkillsSettings() {
{skillSaving ? (
<>
<Loader2 className="h-3 w-3 animate-spin" />
...
{t("actions.saving")}
</>
) : (
<>
<Save className="h-3 w-3" />
{t("actions.save")}
</>
)}
</Button>
@@ -981,7 +946,7 @@ export function SkillsSettings() {
<div className="rounded-md border p-3 space-y-2.5">
<div className="text-[11px] text-muted-foreground flex items-center gap-1">
<BookOpenText className="h-3.5 w-3.5" />
Skill
{t("skillInfo")}
</div>
<Input
@@ -989,26 +954,30 @@ export function SkillsSettings() {
onChange={(event) => {
setSkillDraftId(event.target.value)
}}
placeholder="skill-id (letters/numbers/-/_/.)"
placeholder={t("skillIdPlaceholder")}
/>
{draftPathPreview ? (
<div className="text-[11px] text-muted-foreground break-all">
Skills目录{draftPathPreview}
{t("skillsDirectoryWithPath", {
path: draftPathPreview,
})}
</div>
) : (
<div className="text-[11px] text-muted-foreground break-all">
Skills目录 Skill ID
{t("skillsDirectoryNeedId")}
</div>
)}
</div>
<div className="rounded-md border p-3 space-y-2">
<div className="text-[11px] text-muted-foreground flex items-center justify-between gap-2">
<span>Markdown </span>
<span>{t("markdownContent")}</span>
<div className="flex items-center gap-1.5">
<span>
{isContentEditing ? "编辑中" : "预览中"}
{isContentEditing
? t("editingStatus")
: t("previewStatus")}
</span>
<Button
size="xs"
@@ -1023,12 +992,12 @@ export function SkillsSettings() {
{isContentEditing ? (
<>
<Eye className="h-3 w-3" />
{t("actions.preview")}
</>
) : (
<>
<Pencil className="h-3 w-3" />
{t("actions.edit")}
</>
)}
</Button>
@@ -1041,7 +1010,7 @@ export function SkillsSettings() {
onChange={(event) => {
setSkillDraftContent(event.target.value)
}}
placeholder="输入 Skill 文本内容..."
placeholder={t("contentPlaceholder")}
className="min-h-80 font-mono text-xs"
/>
) : (
@@ -1049,7 +1018,7 @@ export function SkillsSettings() {
{parsedPreviewContent.frontMatterRaw && (
<div className="rounded-md border bg-muted/10 p-3">
<div className="text-[11px] text-muted-foreground mb-2">
Skills
{t("metadataTitle")}
</div>
{parsedPreviewContent.fields.length > 0 ? (
<div className="grid gap-1.5">
@@ -1097,11 +1066,11 @@ export function SkillsSettings() {
</div>
) : parsedPreviewContent.frontMatterRaw ? (
<div className="text-xs text-muted-foreground py-3">
Skill YAML
{t("onlyYamlMetadata")}
</div>
) : (
<div className="text-xs text-muted-foreground py-3">
{t("emptyContentHint")}
</div>
)}
</div>
@@ -1110,7 +1079,7 @@ export function SkillsSettings() {
{skillReading && (
<div className="text-[11px] text-muted-foreground">
Skill...
{t("loadingSkill")}
</div>
)}
</div>
@@ -1118,7 +1087,7 @@ export function SkillsSettings() {
</div>
) : (
<div className="h-full flex items-center justify-center text-xs text-muted-foreground">
Agent
{t("emptyNoAgents")}
</div>
)}
</div>
@@ -1138,21 +1107,22 @@ export function SkillsSettings() {
>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle> Skill</AlertDialogTitle>
<AlertDialogTitle>{t("deleteDialog.title")}</AlertDialogTitle>
<AlertDialogDescription>
{deleteTargetSkill ? (
<>
Skill <code>{deleteTargetSkill.name}</code>{" "}
{t("deleteDialog.confirmWithNamePrefix")}{" "}
<code>{deleteTargetSkill.name}</code>{" "}
{t("deleteDialog.confirmWithNameSuffix")}
</>
) : (
"确认删除当前 Skill 吗?该操作无法撤销。"
t("deleteDialog.confirm")
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={Boolean(skillDeletingId)}>
{t("actions.cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
@@ -1163,7 +1133,7 @@ export function SkillsSettings() {
})
}}
>
{skillDeletingId ? "删除中..." : "删除"}
{skillDeletingId ? t("actions.deleting") : t("actions.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -69,6 +69,402 @@
"installSuccess": "Update installed. Relaunching app.",
"installFailed": "Update failed: {message}"
},
"ShortcutSettings": {
"sectionTitle": "Shortcuts",
"resetDefault": "Reset defaults",
"recordInstruction": "Click the right-side button, then press a key combination. Use Ctrl/Cmd, Alt, and Shift. Press Esc to cancel recording.",
"recording": "Press shortcut...",
"toasts": {
"conflict": "Shortcut is already used by \"{title}\"",
"updated": "Shortcut updated",
"invalid": "Invalid shortcut, please try again",
"reset": "Default shortcuts restored"
},
"actions": {
"toggle_search": {
"title": "Open Search",
"description": "Show or hide the conversation search panel"
},
"toggle_sidebar": {
"title": "Toggle Left Sidebar",
"description": "Show or hide the conversation list sidebar"
},
"toggle_terminal": {
"title": "Toggle Terminal",
"description": "Show or hide the bottom terminal panel"
},
"new_terminal_tab": {
"title": "New Terminal",
"description": "Create a new terminal tab when focus is on terminal"
},
"close_current_terminal_tab": {
"title": "Close Current Terminal",
"description": "Close the current terminal tab when focus is on terminal"
},
"toggle_aux_panel": {
"title": "Toggle Right Panel",
"description": "Show or hide the auxiliary info panel"
},
"new_conversation": {
"title": "New Conversation",
"description": "Create a new conversation tab in current folder"
},
"open_folder": {
"title": "Open Folder",
"description": "Open folder picker and open in a new window"
},
"open_settings": {
"title": "Open Settings",
"description": "Open settings window"
},
"close_current_tab": {
"title": "Close Current Tab",
"description": "Close current conversation or file tab"
},
"close_all_file_tabs": {
"title": "Close All File Tabs",
"description": "Close all file tabs in file mode only"
}
}
},
"SkillsSettings": {
"title": "Skills",
"description": "Select a skill on the left. The right side previews Markdown by default; switch to edit to modify and save.",
"loadingAgents": "Loading agents that support Skills...",
"emptyNoManageableAgents": "No agents available for Skills management.",
"managedTarget": "Managed target",
"selectAgentPlaceholder": "Select an agent",
"searchPlaceholder": "Search by name / ID / path...",
"skillsList": "Skills list",
"loadingSkills": "Loading skills...",
"agentNotSupported": "Current agent does not support Skills management.",
"emptySkills": "No skills yet. Click \"New Skill\" to create one.",
"newSkillTitle": "New Skill",
"skillInfo": "Skill Info",
"skillIdPlaceholder": "skill-id (letters/numbers/-/_/.)",
"skillsDirectoryWithPath": "Skills directory: {path}",
"skillsDirectoryNeedId": "Skills directory: enter Skill ID to generate full path",
"markdownContent": "Markdown Content",
"editingStatus": "Editing",
"previewStatus": "Previewing",
"contentPlaceholder": "Enter Skill markdown content...",
"metadataTitle": "Skills Metadata",
"onlyYamlMetadata": "This skill only contains YAML metadata.",
"emptyContentHint": "No content yet. Click \"Edit\" to start.",
"loadingSkill": "Loading skill...",
"emptyNoAgents": "No available agent.",
"actions": {
"preview": "Preview",
"edit": "Edit",
"openInWindow": "Open in new window",
"delete": "Delete",
"deleting": "Deleting...",
"refresh": "Refresh",
"newSkill": "New Skill",
"reset": "Reset",
"save": "Save",
"saving": "Saving...",
"cancel": "Cancel"
},
"deleteDialog": {
"title": "Delete Skill",
"confirm": "Delete current skill? This action cannot be undone.",
"confirmWithNamePrefix": "Delete skill",
"confirmWithNameSuffix": "? This action cannot be undone."
},
"toasts": {
"loadFailed": "Failed to load skill",
"openFolderFailed": "Failed to open folder",
"noSkillDirectory": "No available Skills directory found for current agent",
"nameRequired": "Skill name cannot be empty",
"updated": "Skill updated",
"created": "Skill created",
"saveFailed": "Failed to save skill",
"deleted": "Skill deleted",
"deleteFailed": "Failed to delete skill"
},
"templates": {
"gemini": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Skill Name\n\nInstructions for the agent when this skill is active.\n\n## Workflow\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openCode": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Steps\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openClaw": "---\nname: example-skill\ndescription: Describe when this skill should be used.\nuser-invocable: true\ndisable-model-invocation: false\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n",
"default": "# Skill: example-skill\n\n## When to use\n\n- Describe trigger conditions.\n\n## Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n"
}
},
"McpSettings": {
"loading": "Loading...",
"summary": {
"missingCommand": "(missing command)",
"missingUrl": "(missing url)"
},
"protocol": {
"stdio": "Stdio"
},
"errors": {
"selectInstallProtocol": "Please select an install protocol",
"fieldRequired": "{field} is required",
"fieldNeedsBoolean": "{field} must be true or false",
"fieldNeedsNumber": "{field} must be a number",
"fieldNeedsInteger": "{field} must be an integer",
"fieldInvalidJson": "{field} has invalid JSON: {message}",
"fieldOutOfRange": "{field} value is out of allowed range",
"jsonEmpty": "{name} cannot be empty",
"jsonInvalid": "{name} is not valid JSON: {message}",
"jsonMustBeObject": "{name} must be a JSON object"
},
"jsonNames": {
"localConfig": "MCP Config",
"installConfig": "Install Config"
},
"toasts": {
"uninstalled": "MCP uninstalled",
"uninstallFailed": "Uninstall failed: {message}",
"selectAtLeastOneApp": "Please select at least one target app",
"saveSuccess": "Saved",
"saveFailed": "Save failed: {message}",
"installed": "{name} installed",
"installFailed": "Install failed: {message}"
},
"installDialog": {
"title": "Confirm MCP Installation",
"descriptionWithName": "Install {name} to local configuration.",
"description": "Select target apps for installation.",
"protocol": "Protocol",
"selectProtocol": "Select protocol",
"parameters": "Configuration Parameters",
"booleanPlaceholder": "Please select true/false",
"selectOneValue": "Select a value",
"targetApps": "Target Apps"
},
"actions": {
"cancel": "Cancel",
"confirmInstall": "Confirm Install",
"installing": "Installing",
"uninstall": "Uninstall",
"uninstalling": "Uninstalling",
"viewDetails": "View Details",
"save": "Save",
"saving": "Saving",
"install": "Install"
},
"tabs": {
"local": "Local MCP",
"market": "MCP Marketplace"
},
"local": {
"filterPlaceholder": "Filter local MCP...",
"loadFailed": "Load failed: {message}",
"empty": "No local MCP detected.",
"description": "Local MCP configuration can be edited and saved directly.",
"enabledApps": "Enabled Apps",
"configJson": "MCP Config (JSON)"
},
"market": {
"selectMarketplace": "Select marketplace",
"searchPlaceholder": "Search MCP...",
"searchFailed": "Search failed: {message}",
"loadingList": "Loading MCP list...",
"empty": "No MCP results.",
"loadingDetail": "Loading marketplace details...",
"detailLoadFailed": "Failed to load details: {message}",
"owner": "Owner: {owner}",
"namespace": "Namespace: {namespace}",
"defaultInstallProtocol": "Default Install Protocol",
"currentOptionParameterCount": "Current option parameter count: {count}",
"installConfigDescription": "Install Config (JSON, editable before install; edits will override protocol/parameter form)",
"selectLeftToView": "Select a marketplace MCP on the left to view details."
},
"badges": {
"verified": "Verified",
"remote": "Remote",
"hasHomepage": "Has Homepage",
"uses": "{count} uses",
"deployed": "Deployed",
"notDeployed": "Not Deployed"
},
"selectLeftMcp": "Select an MCP on the left."
},
"AcpAgentSettings": {
"title": "Agent SDK Management",
"description": "Manage Agent SDK connection, enabled state, environment variables, config management and version preflight info in one place.",
"loadingAgents": "Loading agent list...",
"agentList": "Agent List",
"emptyNoAgent": "No available agent.",
"configManagement": "Config Management",
"envVars": "Environment Variables",
"nativeJsonConfig": "Native JSON Config",
"modelHintDefault": "Leave empty to use system default model.",
"generalConfigDescriptionClaude": "Supports quick configuration for API URL, API Key and Claude models, and syncs with native JSON config.",
"generalConfigDescriptionDefault": "Supports important config input (API URL, API Key, Model) and native JSON config management.",
"actions": {
"dragSort": "Drag to reorder",
"dragSortAgent": "Drag to reorder {name}",
"refreshCheck": "Refresh check",
"refreshCheckAgent": "Refresh check {name}",
"clickEnable": "Click to enable {name}",
"clickDisable": "Click to disable {name}",
"install": "Install",
"upgrade": "Upgrade",
"uninstall": "Uninstall",
"uninstalling": "Uninstalling...",
"saveEnvVars": "Save environment variables",
"saving": "Saving...",
"saveCodexConfig": "Save Codex Config",
"saveGeminiConfig": "Save Gemini Config",
"saveOpenCodeConfig": "Save OpenCode Config",
"saveOpenClawConfig": "Save OpenClaw Config",
"saveConfigManagement": "Save Config Management",
"saveCurrentProvider": "Save Current Provider",
"showApiKey": "Show API Key",
"hideApiKey": "Hide API Key",
"showKey": "Show Key",
"hideKey": "Hide Key",
"showToken": "Show Token",
"hideToken": "Hide Token",
"cancel": "Cancel",
"delete": "Delete",
"deleting": "Deleting...",
"confirmDelete": "Confirm Delete",
"confirmUninstall": "Confirm Uninstall"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"unchecked": "Unchecked",
"agentEnabledAria": "{name} enabled",
"agentEnabledSwitch": "{name} enable switch"
},
"preflight": {
"count": "Preflight items: {count}",
"notRun": "Checks have not run yet."
},
"codex": {
"configDescription": "Supports quick configuration for API URL, API Key, model name and reasoning effort, and syncs with `auth.json` / `config.toml`.",
"selectProvider": "Select Provider",
"modelName": "Model Name",
"selectReasoningEffort": "Select Reasoning Effort",
"enableWebsocket": "Enable WebSocket",
"enableWebsocketAria": "Enable WebSocket for Codex Provider",
"authJsonNative": "auth.json (native)",
"configTomlNative": "config.toml (native)"
},
"gemini": {
"authConfig": "Gemini Auth Config",
"authConfigDescription": "Aligned with Gemini CLI authentication docs, supporting custom endpoint, Google login, Gemini API Key and Vertex AI (ADC / service account / API Key).",
"authMode": "Auth Mode",
"selectAuthMode": "Select auth mode",
"viewAuthDoc": "View auth docs",
"mode": {
"custom": "Custom Endpoint",
"loginGoogle": "Google Login (OAuth)",
"vertexServiceAccount": "Vertex AI (Service Account)"
},
"hint": {
"custom": "Fill API URL, API Key and Model, mapped to GOOGLE_GEMINI_BASE_URL / GEMINI_API_KEY / GEMINI_MODEL.",
"loginGoogle": "Run gemini in terminal and complete Google login first; API key is not required.",
"geminiApiKey": "Fill GEMINI_API_KEY when using Gemini API.",
"vertexAdc": "Use gcloud ADC; GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are recommended.",
"vertexServiceAccount": "Set service account JSON path to GOOGLE_APPLICATION_CREDENTIALS.",
"vertexApiKey": "Fill GOOGLE_API_KEY when using Vertex AI API key."
}
},
"openCode": {
"configManagement": "OpenCode Config Management",
"configDescription": "Aligned with OpenCode `provider` schema, supports multi-provider management and two-way sync with native JSON files.",
"providerManagement": "Provider Management",
"providerCount": "{count} providers",
"addProvider": "Add Provider",
"emptyProvider": "No provider yet. Enter an ID then click \"Add Provider\".",
"providerEnabledState": "{providerId} enabled state",
"selectProviderNpm": "Select provider.npm",
"notSet": "Not set",
"modelManagement": "Model Management",
"modelCount": "{count} models",
"modelDescription": "Aligned with OpenCode `provider.models`. Fast management currently supports `name` / `id`; other advanced fields are preserved and can be edited in native JSON below.",
"addModel": "Add model",
"emptyModel": "No model yet. Enter model id then click \"Add model\".",
"modelId": "Model ID",
"modelName": "Model Name",
"deleteModel": "Delete model {modelId}",
"nativeJsonConfig": "OpenCode Native JSON Config"
},
"openClaw": {
"gatewayConfig": "Gateway Config",
"gatewayDescription": "Configure OpenClaw Gateway connection. Supports local or remote gateway.",
"gatewayUrlHint": "Leave empty to use gateway.remote.url from local openclaw config.",
"gatewayTokenPlaceholder": "Gateway auth token",
"gatewayTokenHint": "Use token-file instead of plain token when possible; configure via openclaw CLI.",
"sessionKeyHint": "Optional. Specify gateway session key; leave empty to auto-assign isolated session."
},
"claude": {
"mainModel": "Main Model",
"reasoningModel": "Reasoning Model (thinking)",
"haikuDefaultModel": "Default Haiku Model",
"sonnetDefaultModel": "Default Sonnet Model",
"opusDefaultModel": "Default Opus Model"
},
"dialogs": {
"confirmDeleteProvider": "Delete Provider {providerId}?",
"confirmDeleteProviderDescription": "OpenCode config and auth JSON will be updated together. This action cannot be undone.",
"confirmUninstall": "Uninstall {name}?",
"confirmUninstallDescription": "This removes local installed version. You can reinstall later."
},
"errors": {
"nativeJsonMustBeObject": "Native JSON config must be an object",
"nativeJsonInvalid": "Native JSON config format error: {message}",
"openCodeAuthMustBeObject": "OpenCode auth.json must be a JSON object",
"openCodeAuthInvalid": "OpenCode auth.json format error: {message}",
"authMustBeObject": "auth.json must be a JSON object",
"authInvalid": "auth.json format error: {message}",
"providerIdPattern": "Provider ID only supports letters, numbers, underscore, dot and hyphen",
"providerExists": "Provider '{providerId}' already exists",
"modelIdPattern": "Model ID only supports letters, numbers, underscore, dot, colon and hyphen",
"modelExists": "Model '{modelId}' already exists"
},
"warnings": {
"nativeJsonRecoveredStructured": "Native JSON config is invalid; reset to structured config",
"nativeJsonRecoveredOpenCode": "Native JSON config is invalid; reset to OpenCode structured config",
"openCodeAuthRecovered": "OpenCode auth.json is invalid; reset to default config",
"authRecoveredStructured": "auth.json is invalid; reset to structured config"
},
"toasts": {
"agentActionCompleted": "{name} {action} completed",
"agentActionFailed": "{name} {action} failed",
"localVersion": "Local version: {version}",
"installCompletedVersionLater": "Install completed, version will update on next check",
"uninstallCompleted": "{name} uninstall completed",
"uninstallFailed": "{name} uninstall failed",
"localVersionRemoved": "Local version removed",
"saveAgentOrderFailed": "Failed to save Agent order",
"saveAgentSwitchFailed": "Failed to save Agent switch",
"saveEnvFailed": "Failed to save environment variables",
"codexSaved": "Codex config saved",
"saveCodexNativeFailed": "Failed to save Codex native config",
"geminiSaved": "Gemini config saved",
"saveGeminiFailed": "Failed to save Gemini config",
"providerDeleted": "Provider {providerId} deleted",
"providerDeleteFailed": "Failed to delete Provider {providerId}",
"providerSaved": "Provider {providerId} saved",
"saveProviderFailed": "Failed to save Provider {providerId}",
"openCodeConfigSynced": "OpenCode config and auth JSON have been synced.",
"openCodeSaved": "OpenCode config saved",
"saveOpenCodeFailed": "Failed to save OpenCode config",
"openClawSaved": "OpenClaw config saved",
"saveOpenClawFailed": "Failed to save OpenClaw config",
"configSaved": "Config saved",
"saveConfigManagementFailed": "Failed to save config management"
},
"version": {
"statusLabel": "Version Status",
"notInstalled": "Not installed",
"remoteLocal": "Remote: {remoteVersion} · Local: {localVersion}",
"platformUnsupported": "{versionText}. Current platform does not support this agent.",
"clickInstall": "{versionText}. Click Install on the right.",
"localUnrecognized": "{versionText}. Local version is not comparable; try upgrade to overwrite install.",
"upgradeAvailable": "{versionText}. Upgrade available.",
"remoteUnavailable": "{versionText}. Remote version is currently unavailable.",
"latest": "{versionText}. Already latest."
}
},
"SettingsPages": {
"agentsLoading": "Loading agent settings..."
}

View File

@@ -69,6 +69,402 @@
"installSuccess": "升级包已安装,正在重启应用",
"installFailed": "升级失败:{message}"
},
"ShortcutSettings": {
"sectionTitle": "快捷键",
"resetDefault": "恢复默认",
"recordInstruction": "点击右侧按钮后按下组合键即可修改。建议使用 Ctrl/Cmd、Alt、Shift 的组合。按 Esc 可取消录制。",
"recording": "按下快捷键...",
"toasts": {
"conflict": "快捷键已被「{title}」占用",
"updated": "快捷键已更新",
"invalid": "快捷键无效,请重试",
"reset": "已恢复默认快捷键"
},
"actions": {
"toggle_search": {
"title": "打开搜索",
"description": "打开或关闭会话搜索面板"
},
"toggle_sidebar": {
"title": "切换左侧边栏",
"description": "显示或隐藏会话列表侧边栏"
},
"toggle_terminal": {
"title": "切换终端",
"description": "显示或隐藏底部终端面板"
},
"new_terminal_tab": {
"title": "新建终端",
"description": "当最近鼠标活动在终端时新建终端标签"
},
"close_current_terminal_tab": {
"title": "关闭当前终端",
"description": "当最近鼠标活动在终端时关闭当前终端标签"
},
"toggle_aux_panel": {
"title": "切换右侧面板",
"description": "显示或隐藏辅助信息面板"
},
"new_conversation": {
"title": "新建会话",
"description": "在当前文件夹中创建新的对话标签"
},
"open_folder": {
"title": "打开文件夹",
"description": "打开文件夹选择器并在新窗口中打开"
},
"open_settings": {
"title": "打开设置",
"description": "打开设置窗口"
},
"close_current_tab": {
"title": "关闭当前标签",
"description": "关闭当前会话或文件标签"
},
"close_all_file_tabs": {
"title": "关闭全部文件标签",
"description": "仅在文件模式下关闭所有文件标签"
}
}
},
"SkillsSettings": {
"title": "Skills",
"description": "左侧选择 Skill右侧默认预览 Markdown点击编辑后可修改并保存。",
"loadingAgents": "正在加载支持 Skills 的 Agent...",
"emptyNoManageableAgents": "当前没有可管理 Skills 的 Agent。",
"managedTarget": "管理对象",
"selectAgentPlaceholder": "请选择 Agent",
"searchPlaceholder": "搜索名称 / ID / 路径...",
"skillsList": "Skills 列表",
"loadingSkills": "加载 Skills 中...",
"agentNotSupported": "当前 Agent 暂不支持 Skills 管理。",
"emptySkills": "暂无 Skill可点击“新建 Skill”。",
"newSkillTitle": "新建 Skill",
"skillInfo": "Skill 信息",
"skillIdPlaceholder": "skill-id (letters/numbers/-/_/.)",
"skillsDirectoryWithPath": "Skills目录{path}",
"skillsDirectoryNeedId": "Skills目录请输入 Skill ID 以生成完整路径",
"markdownContent": "Markdown 内容",
"editingStatus": "编辑中",
"previewStatus": "预览中",
"contentPlaceholder": "输入 Skill 文本内容...",
"metadataTitle": "Skills 元信息",
"onlyYamlMetadata": "该 Skill 仅包含 YAML 元信息。",
"emptyContentHint": "暂无内容。点击“编辑”开始输入。",
"loadingSkill": "正在加载 Skill...",
"emptyNoAgents": "暂无可用 Agent。",
"actions": {
"preview": "预览",
"edit": "编辑",
"openInWindow": "在新窗口打开",
"delete": "删除",
"deleting": "删除中...",
"refresh": "刷新",
"newSkill": "新建 Skill",
"reset": "重置",
"save": "保存",
"saving": "保存中...",
"cancel": "取消"
},
"deleteDialog": {
"title": "删除 Skill",
"confirm": "确认删除当前 Skill 吗?该操作无法撤销。",
"confirmWithNamePrefix": "确认删除 Skill",
"confirmWithNameSuffix": "吗?该操作无法撤销。"
},
"toasts": {
"loadFailed": "加载 Skill 失败",
"openFolderFailed": "打开目录失败",
"noSkillDirectory": "当前 Agent 未找到可用的 Skills 目录",
"nameRequired": "Skill 名称不能为空",
"updated": "Skill 已更新",
"created": "Skill 已创建",
"saveFailed": "保存 Skill 失败",
"deleted": "Skill 已删除",
"deleteFailed": "删除 Skill 失败"
},
"templates": {
"gemini": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Skill Name\n\nInstructions for the agent when this skill is active.\n\n## Workflow\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openCode": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Steps\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openClaw": "---\nname: example-skill\ndescription: Describe when this skill should be used.\nuser-invocable: true\ndisable-model-invocation: false\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n",
"default": "# Skill: example-skill\n\n## When to use\n\n- Describe trigger conditions.\n\n## Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n"
}
},
"McpSettings": {
"loading": "加载中...",
"summary": {
"missingCommand": "(缺少 command)",
"missingUrl": "(缺少 url)"
},
"protocol": {
"stdio": "Stdio"
},
"errors": {
"selectInstallProtocol": "请选择安装协议",
"fieldRequired": "{field} 为必填项",
"fieldNeedsBoolean": "{field} 需要 true 或 false",
"fieldNeedsNumber": "{field} 需要数字",
"fieldNeedsInteger": "{field} 需要整数",
"fieldInvalidJson": "{field} JSON 无效:{message}",
"fieldOutOfRange": "{field} 的值不在可选范围内",
"jsonEmpty": "{name} 不能为空",
"jsonInvalid": "{name} 不是合法 JSON{message}",
"jsonMustBeObject": "{name} 必须是 JSON 对象"
},
"jsonNames": {
"localConfig": "MCP 配置",
"installConfig": "安装配置"
},
"toasts": {
"uninstalled": "已卸载 MCP",
"uninstallFailed": "卸载失败:{message}",
"selectAtLeastOneApp": "请至少选择一个目标应用",
"saveSuccess": "保存成功",
"saveFailed": "保存失败:{message}",
"installed": "已安装 {name}",
"installFailed": "安装失败:{message}"
},
"installDialog": {
"title": "确认安装 MCP",
"descriptionWithName": "将 {name} 安装到本地配置。",
"description": "选择安装目标应用。",
"protocol": "协议",
"selectProtocol": "选择协议",
"parameters": "配置参数",
"booleanPlaceholder": "请选择 true/false",
"selectOneValue": "选择一个值",
"targetApps": "目标应用"
},
"actions": {
"cancel": "取消",
"confirmInstall": "确认安装",
"installing": "安装中",
"uninstall": "卸载",
"uninstalling": "卸载中",
"viewDetails": "查看详情",
"save": "保存",
"saving": "保存中",
"install": "安装"
},
"tabs": {
"local": "本地 MCP",
"market": "MCP 市场"
},
"local": {
"filterPlaceholder": "筛选本地 MCP...",
"loadFailed": "加载失败:{message}",
"empty": "当前未检测到本地 MCP。",
"description": "本地 MCP 配置可直接编辑并保存。",
"enabledApps": "启用应用",
"configJson": "MCP 配置(JSON)"
},
"market": {
"selectMarketplace": "选择市场",
"searchPlaceholder": "搜索 MCP...",
"searchFailed": "搜索失败:{message}",
"loadingList": "加载 MCP 列表...",
"empty": "暂无 MCP 结果。",
"loadingDetail": "加载市场详情...",
"detailLoadFailed": "加载详情失败:{message}",
"owner": "Owner: {owner}",
"namespace": "Namespace: {namespace}",
"defaultInstallProtocol": "默认安装协议",
"currentOptionParameterCount": "当前选项参数数:{count}",
"installConfigDescription": "安装配置(JSON可修改后安装修改后将覆盖协议/参数表单)",
"selectLeftToView": "请选择左侧市场 MCP 查看详情。"
},
"badges": {
"verified": "Verified",
"remote": "Remote",
"hasHomepage": "Has Homepage",
"uses": "{count} uses",
"deployed": "Deployed",
"notDeployed": "Not Deployed"
},
"selectLeftMcp": "请选择左侧 MCP。"
},
"AcpAgentSettings": {
"title": "Agent SDK管理",
"description": "统一管理 Agent 的连接SDK、启用状态、环境变量、配置管理与版本预检信息。",
"loadingAgents": "加载 Agent 列表中...",
"agentList": "Agent 列表",
"emptyNoAgent": "暂无可用 Agent。",
"configManagement": "配置管理",
"envVars": "环境变量",
"nativeJsonConfig": "原生 JSON 配置",
"modelHintDefault": "留空则使用系统默认模型。",
"generalConfigDescriptionClaude": "支持 API URL、API Key 与 Claude 模型快捷配置,并与原生 JSON 配置联动。",
"generalConfigDescriptionDefault": "支持重要配置输入API URL、API Key、Model和原生 JSON 配置管理。",
"actions": {
"dragSort": "拖拽排序",
"dragSortAgent": "拖拽排序 {name}",
"refreshCheck": "刷新检测",
"refreshCheckAgent": "刷新检测 {name}",
"clickEnable": "点击启用 {name}",
"clickDisable": "点击禁用 {name}",
"install": "安装",
"upgrade": "升级",
"uninstall": "卸载",
"uninstalling": "卸载中...",
"saveEnvVars": "保存环境变量",
"saving": "保存中...",
"saveCodexConfig": "保存 Codex 配置",
"saveGeminiConfig": "保存 Gemini 配置",
"saveOpenCodeConfig": "保存 OpenCode 配置",
"saveOpenClawConfig": "保存 OpenClaw 配置",
"saveConfigManagement": "保存配置管理",
"saveCurrentProvider": "保存当前 Provider",
"showApiKey": "显示 API Key",
"hideApiKey": "隐藏 API Key",
"showKey": "显示 Key",
"hideKey": "隐藏 Key",
"showToken": "显示 Token",
"hideToken": "隐藏 Token",
"cancel": "取消",
"delete": "删除",
"deleting": "删除中...",
"confirmDelete": "确认删除",
"confirmUninstall": "确认卸载"
},
"status": {
"enabled": "启用",
"disabled": "禁用",
"unchecked": "未检测",
"agentEnabledAria": "{name} 已启用",
"agentEnabledSwitch": "{name} 启用"
},
"preflight": {
"count": "预检项:{count}",
"notRun": "尚未执行检测。"
},
"codex": {
"configDescription": "支持 API URL、API Key、模型名称、Reasoning Effort 快捷配置,并与 `auth.json` / `config.toml` 双向联动。",
"selectProvider": "选择 Provider",
"modelName": "模型名称",
"selectReasoningEffort": "选择 Reasoning Effort",
"enableWebsocket": "启用 WebSocket",
"enableWebsocketAria": "Codex Provider 启用 WebSocket",
"authJsonNative": "auth.json原生",
"configTomlNative": "config.toml原生"
},
"gemini": {
"authConfig": "Gemini 认证配置",
"authConfigDescription": "对齐 Gemini CLI 认证文档支持自定义、Google 登录、Gemini API Key、Vertex AIADC / 服务账号 / API Key。",
"authMode": "认证方式",
"selectAuthMode": "选择认证方式",
"viewAuthDoc": "查看认证文档",
"mode": {
"custom": "自定义接口",
"loginGoogle": "Google 登录OAuth",
"vertexServiceAccount": "Vertex AI服务账号"
},
"hint": {
"custom": "填写 API URL、API Key 和 Model分别映射到 GOOGLE_GEMINI_BASE_URL / GEMINI_API_KEY / GEMINI_MODEL。",
"loginGoogle": "首次在终端运行 gemini 并完成 Google 登录;无需填写 API Key。",
"geminiApiKey": "使用 Gemini API 时填写 GEMINI_API_KEY。",
"vertexAdc": "使用 gcloud ADC建议填写 GOOGLE_CLOUD_PROJECT 与 GOOGLE_CLOUD_LOCATION。",
"vertexServiceAccount": "服务账号 JSON 路径写入 GOOGLE_APPLICATION_CREDENTIALS。",
"vertexApiKey": "使用 Vertex AI API key 时填写 GOOGLE_API_KEY。"
}
},
"openCode": {
"configManagement": "OpenCode 配置管理",
"configDescription": "对齐 OpenCode `provider` 配置结构,支持多供应商管理,并与原生 JSON 文件双向联动。",
"providerManagement": "Provider 管理",
"providerCount": "共 {count} 个",
"addProvider": "新增 Provider",
"emptyProvider": "暂无 Provider输入 ID 后点击“新增 Provider”创建。",
"providerEnabledState": "{providerId} 启用状态",
"selectProviderNpm": "选择 provider.npm",
"notSet": "未设置",
"modelManagement": "模型管理",
"modelCount": "共 {count} 个",
"modelDescription": "对齐 OpenCode `provider.models` 结构。当前支持 `name` / `id` 快速管理;其它高级字段会保留,可在下方原生 JSON 中继续编辑。",
"addModel": "添加模型",
"emptyModel": "暂无模型,输入 model id 后点击“添加模型”创建。",
"modelId": "模型 ID",
"modelName": "模型名称",
"deleteModel": "删除模型 {modelId}",
"nativeJsonConfig": "OpenCode 原生 JSON 配置"
},
"openClaw": {
"gatewayConfig": "Gateway 配置",
"gatewayDescription": "配置 OpenClaw Gateway 连接信息。支持本地或远程 Gateway。",
"gatewayUrlHint": "留空则使用 openclaw 本地配置的 gateway.remote.url。",
"gatewayTokenPlaceholder": "Gateway 认证 Token",
"gatewayTokenHint": "建议使用 token-file 替代明文 Token可通过 openclaw 命令行配置。",
"sessionKeyHint": "可选。指定 Gateway Session Key留空则自动分配隔离会话。"
},
"claude": {
"mainModel": "主模型",
"reasoningModel": "推理模型thinking",
"haikuDefaultModel": "Haiku 默认模型",
"sonnetDefaultModel": "Sonnet 默认模型",
"opusDefaultModel": "Opus 默认模型"
},
"dialogs": {
"confirmDeleteProvider": "确认删除 Provider {providerId}",
"confirmDeleteProviderDescription": "将同步更新 OpenCode 配置与认证 JSON 文件,删除后不可恢复。",
"confirmUninstall": "确认卸载 {name}",
"confirmUninstallDescription": "这会移除本地安装版本,之后可随时重新安装。"
},
"errors": {
"nativeJsonMustBeObject": "原生 JSON 配置必须是对象",
"nativeJsonInvalid": "原生 JSON 配置格式错误:{message}",
"openCodeAuthMustBeObject": "OpenCode auth.json 必须是 JSON 对象",
"openCodeAuthInvalid": "OpenCode auth.json 格式错误:{message}",
"authMustBeObject": "auth.json 必须是 JSON 对象",
"authInvalid": "auth.json 格式错误:{message}",
"providerIdPattern": "Provider ID 仅支持字母、数字、下划线、点与中划线",
"providerExists": "Provider '{providerId}' 已存在",
"modelIdPattern": "模型 ID 仅支持字母、数字、下划线、点、冒号与中划线",
"modelExists": "Model '{modelId}' 已存在"
},
"warnings": {
"nativeJsonRecoveredStructured": "原生 JSON 配置格式无效,已重置为结构化配置",
"nativeJsonRecoveredOpenCode": "原生 JSON 配置格式无效,已重置为 OpenCode 结构化配置",
"openCodeAuthRecovered": "OpenCode auth.json 格式无效,已重置为默认配置",
"authRecoveredStructured": "auth.json 格式无效,已重置为结构化配置"
},
"toasts": {
"agentActionCompleted": "{name}{action}完成",
"agentActionFailed": "{name}{action}失败",
"localVersion": "本地版本:{version}",
"installCompletedVersionLater": "安装完成,版本将在下一次检测时更新",
"uninstallCompleted": "{name}卸载完成",
"uninstallFailed": "{name}卸载失败",
"localVersionRemoved": "本地版本已移除",
"saveAgentOrderFailed": "保存 Agent 排序失败",
"saveAgentSwitchFailed": "保存 Agent 开关失败",
"saveEnvFailed": "保存环境变量失败",
"codexSaved": "Codex 配置已保存",
"saveCodexNativeFailed": "保存 Codex 原生配置失败",
"geminiSaved": "Gemini 配置已保存",
"saveGeminiFailed": "保存 Gemini 配置失败",
"providerDeleted": "Provider {providerId} 已删除",
"providerDeleteFailed": "删除 Provider {providerId} 失败",
"providerSaved": "Provider {providerId} 保存成功",
"saveProviderFailed": "保存 Provider {providerId} 失败",
"openCodeConfigSynced": "OpenCode 配置与认证 JSON 已同步保存。",
"openCodeSaved": "OpenCode 配置已保存",
"saveOpenCodeFailed": "保存 OpenCode 配置失败",
"openClawSaved": "OpenClaw 配置已保存",
"saveOpenClawFailed": "保存 OpenClaw 配置失败",
"configSaved": "配置已保存",
"saveConfigManagementFailed": "保存配置管理失败"
},
"version": {
"statusLabel": "版本状态",
"notInstalled": "未安装",
"remoteLocal": "远程:{remoteVersion} · 本地:{localVersion}",
"platformUnsupported": "{versionText}。当前平台不支持该 Agent。",
"clickInstall": "{versionText}。请点击右侧安装。",
"localUnrecognized": "{versionText}。本地版本无法识别,可尝试升级覆盖安装。",
"upgradeAvailable": "{versionText}。发现可升级版本。",
"remoteUnavailable": "{versionText}。远程版本暂不可用。",
"latest": "{versionText}。已是最新版本。"
}
},
"SettingsPages": {
"agentsLoading": "加载 Agent 设置中..."
}

View File

@@ -69,6 +69,402 @@
"installSuccess": "升級包已安裝,正在重新啟動應用",
"installFailed": "升級失敗:{message}"
},
"ShortcutSettings": {
"sectionTitle": "快捷鍵",
"resetDefault": "恢復預設",
"recordInstruction": "點擊右側按鈕後按下組合鍵即可修改。建議使用 Ctrl/Cmd、Alt、Shift 的組合。按 Esc 可取消錄製。",
"recording": "按下快捷鍵...",
"toasts": {
"conflict": "快捷鍵已被「{title}」占用",
"updated": "快捷鍵已更新",
"invalid": "快捷鍵無效,請重試",
"reset": "已恢復預設快捷鍵"
},
"actions": {
"toggle_search": {
"title": "打開搜尋",
"description": "打開或關閉會話搜尋面板"
},
"toggle_sidebar": {
"title": "切換左側邊欄",
"description": "顯示或隱藏會話列表側邊欄"
},
"toggle_terminal": {
"title": "切換終端",
"description": "顯示或隱藏底部終端面板"
},
"new_terminal_tab": {
"title": "新增終端",
"description": "當最近滑鼠活動在終端時新增終端分頁"
},
"close_current_terminal_tab": {
"title": "關閉目前終端",
"description": "當最近滑鼠活動在終端時關閉目前終端分頁"
},
"toggle_aux_panel": {
"title": "切換右側面板",
"description": "顯示或隱藏輔助資訊面板"
},
"new_conversation": {
"title": "新增會話",
"description": "在目前資料夾中建立新的對話分頁"
},
"open_folder": {
"title": "打開資料夾",
"description": "打開資料夾選擇器並在新視窗中開啟"
},
"open_settings": {
"title": "打開設定",
"description": "打開設定視窗"
},
"close_current_tab": {
"title": "關閉目前分頁",
"description": "關閉目前會話或檔案分頁"
},
"close_all_file_tabs": {
"title": "關閉全部檔案分頁",
"description": "僅在檔案模式下關閉所有檔案分頁"
}
}
},
"SkillsSettings": {
"title": "Skills",
"description": "左側選擇 Skill右側預設預覽 Markdown點擊編輯後可修改並儲存。",
"loadingAgents": "正在載入支援 Skills 的 Agent...",
"emptyNoManageableAgents": "目前沒有可管理 Skills 的 Agent。",
"managedTarget": "管理對象",
"selectAgentPlaceholder": "請選擇 Agent",
"searchPlaceholder": "搜尋名稱 / ID / 路徑...",
"skillsList": "Skills 列表",
"loadingSkills": "載入 Skills 中...",
"agentNotSupported": "目前 Agent 暫不支援 Skills 管理。",
"emptySkills": "暫無 Skill可點擊「新增 Skill」。",
"newSkillTitle": "新增 Skill",
"skillInfo": "Skill 資訊",
"skillIdPlaceholder": "skill-id (letters/numbers/-/_/.)",
"skillsDirectoryWithPath": "Skills目錄{path}",
"skillsDirectoryNeedId": "Skills目錄請輸入 Skill ID 以產生完整路徑",
"markdownContent": "Markdown 內容",
"editingStatus": "編輯中",
"previewStatus": "預覽中",
"contentPlaceholder": "輸入 Skill 文字內容...",
"metadataTitle": "Skills 中繼資訊",
"onlyYamlMetadata": "該 Skill 僅包含 YAML 中繼資訊。",
"emptyContentHint": "暫無內容。點擊「編輯」開始輸入。",
"loadingSkill": "正在載入 Skill...",
"emptyNoAgents": "暫無可用 Agent。",
"actions": {
"preview": "預覽",
"edit": "編輯",
"openInWindow": "在新視窗打開",
"delete": "刪除",
"deleting": "刪除中...",
"refresh": "刷新",
"newSkill": "新增 Skill",
"reset": "重置",
"save": "儲存",
"saving": "儲存中...",
"cancel": "取消"
},
"deleteDialog": {
"title": "刪除 Skill",
"confirm": "確認刪除目前 Skill 嗎?此操作無法復原。",
"confirmWithNamePrefix": "確認刪除 Skill",
"confirmWithNameSuffix": "嗎?此操作無法復原。"
},
"toasts": {
"loadFailed": "載入 Skill 失敗",
"openFolderFailed": "打開目錄失敗",
"noSkillDirectory": "目前 Agent 未找到可用的 Skills 目錄",
"nameRequired": "Skill 名稱不能為空",
"updated": "Skill 已更新",
"created": "Skill 已建立",
"saveFailed": "儲存 Skill 失敗",
"deleted": "Skill 已刪除",
"deleteFailed": "刪除 Skill 失敗"
},
"templates": {
"gemini": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Skill Name\n\nInstructions for the agent when this skill is active.\n\n## Workflow\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openCode": "---\nname: example-skill\ndescription: Describe when this skill should be used.\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Steps\n\n1. Add actionable step one.\n2. Add actionable step two.\n",
"openClaw": "---\nname: example-skill\ndescription: Describe when this skill should be used.\nuser-invocable: true\ndisable-model-invocation: false\n---\n\n# Purpose\n\nDescribe what this skill helps with.\n\n# Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n",
"default": "# Skill: example-skill\n\n## When to use\n\n- Describe trigger conditions.\n\n## Instructions\n\n1. Add actionable instruction one.\n2. Add actionable instruction two.\n"
}
},
"McpSettings": {
"loading": "載入中...",
"summary": {
"missingCommand": "(缺少 command)",
"missingUrl": "(缺少 url)"
},
"protocol": {
"stdio": "Stdio"
},
"errors": {
"selectInstallProtocol": "請選擇安裝協議",
"fieldRequired": "{field} 為必填項",
"fieldNeedsBoolean": "{field} 需要 true 或 false",
"fieldNeedsNumber": "{field} 需要數字",
"fieldNeedsInteger": "{field} 需要整數",
"fieldInvalidJson": "{field} JSON 無效:{message}",
"fieldOutOfRange": "{field} 的值不在可選範圍內",
"jsonEmpty": "{name} 不能為空",
"jsonInvalid": "{name} 不是合法 JSON{message}",
"jsonMustBeObject": "{name} 必須是 JSON 物件"
},
"jsonNames": {
"localConfig": "MCP 配置",
"installConfig": "安裝配置"
},
"toasts": {
"uninstalled": "已卸載 MCP",
"uninstallFailed": "卸載失敗:{message}",
"selectAtLeastOneApp": "請至少選擇一個目標應用",
"saveSuccess": "儲存成功",
"saveFailed": "儲存失敗:{message}",
"installed": "已安裝 {name}",
"installFailed": "安裝失敗:{message}"
},
"installDialog": {
"title": "確認安裝 MCP",
"descriptionWithName": "將 {name} 安裝到本地配置。",
"description": "選擇安裝目標應用。",
"protocol": "協議",
"selectProtocol": "選擇協議",
"parameters": "配置參數",
"booleanPlaceholder": "請選擇 true/false",
"selectOneValue": "選擇一個值",
"targetApps": "目標應用"
},
"actions": {
"cancel": "取消",
"confirmInstall": "確認安裝",
"installing": "安裝中",
"uninstall": "卸載",
"uninstalling": "卸載中",
"viewDetails": "查看詳情",
"save": "儲存",
"saving": "儲存中",
"install": "安裝"
},
"tabs": {
"local": "本地 MCP",
"market": "MCP 市場"
},
"local": {
"filterPlaceholder": "篩選本地 MCP...",
"loadFailed": "載入失敗:{message}",
"empty": "目前未檢測到本地 MCP。",
"description": "本地 MCP 配置可直接編輯並儲存。",
"enabledApps": "啟用應用",
"configJson": "MCP 配置(JSON)"
},
"market": {
"selectMarketplace": "選擇市場",
"searchPlaceholder": "搜尋 MCP...",
"searchFailed": "搜尋失敗:{message}",
"loadingList": "載入 MCP 列表...",
"empty": "暫無 MCP 結果。",
"loadingDetail": "載入市場詳情...",
"detailLoadFailed": "載入詳情失敗:{message}",
"owner": "Owner: {owner}",
"namespace": "Namespace: {namespace}",
"defaultInstallProtocol": "預設安裝協議",
"currentOptionParameterCount": "目前選項參數數:{count}",
"installConfigDescription": "安裝配置(JSON可修改後安裝修改後將覆蓋協議/參數表單)",
"selectLeftToView": "請選擇左側市場 MCP 查看詳情。"
},
"badges": {
"verified": "Verified",
"remote": "Remote",
"hasHomepage": "Has Homepage",
"uses": "{count} uses",
"deployed": "Deployed",
"notDeployed": "Not Deployed"
},
"selectLeftMcp": "請選擇左側 MCP。"
},
"AcpAgentSettings": {
"title": "Agent SDK管理",
"description": "統一管理 Agent 的連接SDK、啟用狀態、環境變數、配置管理與版本預檢資訊。",
"loadingAgents": "載入 Agent 列表中...",
"agentList": "Agent 列表",
"emptyNoAgent": "暫無可用 Agent。",
"configManagement": "配置管理",
"envVars": "環境變數",
"nativeJsonConfig": "原生 JSON 配置",
"modelHintDefault": "留空則使用系統預設模型。",
"generalConfigDescriptionClaude": "支援 API URL、API Key 與 Claude 模型快捷配置,並與原生 JSON 配置聯動。",
"generalConfigDescriptionDefault": "支援重要配置輸入API URL、API Key、Model和原生 JSON 配置管理。",
"actions": {
"dragSort": "拖拽排序",
"dragSortAgent": "拖拽排序 {name}",
"refreshCheck": "刷新檢測",
"refreshCheckAgent": "刷新檢測 {name}",
"clickEnable": "點擊啟用 {name}",
"clickDisable": "點擊停用 {name}",
"install": "安裝",
"upgrade": "升級",
"uninstall": "卸載",
"uninstalling": "卸載中...",
"saveEnvVars": "儲存環境變數",
"saving": "儲存中...",
"saveCodexConfig": "儲存 Codex 配置",
"saveGeminiConfig": "儲存 Gemini 配置",
"saveOpenCodeConfig": "儲存 OpenCode 配置",
"saveOpenClawConfig": "儲存 OpenClaw 配置",
"saveConfigManagement": "儲存配置管理",
"saveCurrentProvider": "儲存目前 Provider",
"showApiKey": "顯示 API Key",
"hideApiKey": "隱藏 API Key",
"showKey": "顯示 Key",
"hideKey": "隱藏 Key",
"showToken": "顯示 Token",
"hideToken": "隱藏 Token",
"cancel": "取消",
"delete": "刪除",
"deleting": "刪除中...",
"confirmDelete": "確認刪除",
"confirmUninstall": "確認卸載"
},
"status": {
"enabled": "啟用",
"disabled": "停用",
"unchecked": "未檢測",
"agentEnabledAria": "{name} 已啟用",
"agentEnabledSwitch": "{name} 啟用"
},
"preflight": {
"count": "預檢項:{count}",
"notRun": "尚未執行檢測。"
},
"codex": {
"configDescription": "支援 API URL、API Key、模型名稱、Reasoning Effort 快捷配置,並與 `auth.json` / `config.toml` 雙向聯動。",
"selectProvider": "選擇 Provider",
"modelName": "模型名稱",
"selectReasoningEffort": "選擇 Reasoning Effort",
"enableWebsocket": "啟用 WebSocket",
"enableWebsocketAria": "Codex Provider 啟用 WebSocket",
"authJsonNative": "auth.json原生",
"configTomlNative": "config.toml原生"
},
"gemini": {
"authConfig": "Gemini 認證配置",
"authConfigDescription": "對齊 Gemini CLI 認證文件支援自訂、Google 登入、Gemini API Key、Vertex AIADC / 服務帳號 / API Key。",
"authMode": "認證方式",
"selectAuthMode": "選擇認證方式",
"viewAuthDoc": "查看認證文件",
"mode": {
"custom": "自訂介面",
"loginGoogle": "Google 登入OAuth",
"vertexServiceAccount": "Vertex AI服務帳號"
},
"hint": {
"custom": "填寫 API URL、API Key 和 Model分別映射到 GOOGLE_GEMINI_BASE_URL / GEMINI_API_KEY / GEMINI_MODEL。",
"loginGoogle": "首次在終端執行 gemini 並完成 Google 登入;無需填寫 API Key。",
"geminiApiKey": "使用 Gemini API 時填寫 GEMINI_API_KEY。",
"vertexAdc": "使用 gcloud ADC建議填寫 GOOGLE_CLOUD_PROJECT 與 GOOGLE_CLOUD_LOCATION。",
"vertexServiceAccount": "服務帳號 JSON 路徑寫入 GOOGLE_APPLICATION_CREDENTIALS。",
"vertexApiKey": "使用 Vertex AI API key 時填寫 GOOGLE_API_KEY。"
}
},
"openCode": {
"configManagement": "OpenCode 配置管理",
"configDescription": "對齊 OpenCode `provider` 配置結構,支援多供應商管理,並與原生 JSON 檔案雙向聯動。",
"providerManagement": "Provider 管理",
"providerCount": "共 {count} 個",
"addProvider": "新增 Provider",
"emptyProvider": "暫無 Provider輸入 ID 後點擊「新增 Provider」建立。",
"providerEnabledState": "{providerId} 啟用狀態",
"selectProviderNpm": "選擇 provider.npm",
"notSet": "未設定",
"modelManagement": "模型管理",
"modelCount": "共 {count} 個",
"modelDescription": "對齊 OpenCode `provider.models` 結構。目前支援 `name` / `id` 快速管理;其它進階欄位會保留,可在下方原生 JSON 中繼續編輯。",
"addModel": "新增模型",
"emptyModel": "暫無模型,輸入 model id 後點擊「新增模型」建立。",
"modelId": "模型 ID",
"modelName": "模型名稱",
"deleteModel": "刪除模型 {modelId}",
"nativeJsonConfig": "OpenCode 原生 JSON 配置"
},
"openClaw": {
"gatewayConfig": "Gateway 配置",
"gatewayDescription": "配置 OpenClaw Gateway 連線資訊。支援本地或遠端 Gateway。",
"gatewayUrlHint": "留空則使用 openclaw 本地配置的 gateway.remote.url。",
"gatewayTokenPlaceholder": "Gateway 認證 Token",
"gatewayTokenHint": "建議使用 token-file 取代明文 Token可透過 openclaw 命令列配置。",
"sessionKeyHint": "可選。指定 Gateway Session Key留空則自動分配隔離會話。"
},
"claude": {
"mainModel": "主模型",
"reasoningModel": "推理模型thinking",
"haikuDefaultModel": "Haiku 預設模型",
"sonnetDefaultModel": "Sonnet 預設模型",
"opusDefaultModel": "Opus 預設模型"
},
"dialogs": {
"confirmDeleteProvider": "確認刪除 Provider {providerId}",
"confirmDeleteProviderDescription": "將同步更新 OpenCode 配置與認證 JSON 檔案,刪除後不可恢復。",
"confirmUninstall": "確認卸載 {name}",
"confirmUninstallDescription": "這會移除本地安裝版本,之後可隨時重新安裝。"
},
"errors": {
"nativeJsonMustBeObject": "原生 JSON 配置必須是物件",
"nativeJsonInvalid": "原生 JSON 配置格式錯誤:{message}",
"openCodeAuthMustBeObject": "OpenCode auth.json 必須是 JSON 物件",
"openCodeAuthInvalid": "OpenCode auth.json 格式錯誤:{message}",
"authMustBeObject": "auth.json 必須是 JSON 物件",
"authInvalid": "auth.json 格式錯誤:{message}",
"providerIdPattern": "Provider ID 僅支援字母、數字、底線、點與中劃線",
"providerExists": "Provider '{providerId}' 已存在",
"modelIdPattern": "模型 ID 僅支援字母、數字、底線、點、冒號與中劃線",
"modelExists": "Model '{modelId}' 已存在"
},
"warnings": {
"nativeJsonRecoveredStructured": "原生 JSON 配置格式無效,已重置為結構化配置",
"nativeJsonRecoveredOpenCode": "原生 JSON 配置格式無效,已重置為 OpenCode 結構化配置",
"openCodeAuthRecovered": "OpenCode auth.json 格式無效,已重置為預設配置",
"authRecoveredStructured": "auth.json 格式無效,已重置為結構化配置"
},
"toasts": {
"agentActionCompleted": "{name}{action}完成",
"agentActionFailed": "{name}{action}失敗",
"localVersion": "本地版本:{version}",
"installCompletedVersionLater": "安裝完成,版本將在下一次檢測時更新",
"uninstallCompleted": "{name}卸載完成",
"uninstallFailed": "{name}卸載失敗",
"localVersionRemoved": "本地版本已移除",
"saveAgentOrderFailed": "儲存 Agent 排序失敗",
"saveAgentSwitchFailed": "儲存 Agent 開關失敗",
"saveEnvFailed": "儲存環境變數失敗",
"codexSaved": "Codex 配置已儲存",
"saveCodexNativeFailed": "儲存 Codex 原生配置失敗",
"geminiSaved": "Gemini 配置已儲存",
"saveGeminiFailed": "儲存 Gemini 配置失敗",
"providerDeleted": "Provider {providerId} 已刪除",
"providerDeleteFailed": "刪除 Provider {providerId} 失敗",
"providerSaved": "Provider {providerId} 儲存成功",
"saveProviderFailed": "儲存 Provider {providerId} 失敗",
"openCodeConfigSynced": "OpenCode 配置與認證 JSON 已同步儲存。",
"openCodeSaved": "OpenCode 配置已儲存",
"saveOpenCodeFailed": "儲存 OpenCode 配置失敗",
"openClawSaved": "OpenClaw 配置已儲存",
"saveOpenClawFailed": "儲存 OpenClaw 配置失敗",
"configSaved": "配置已儲存",
"saveConfigManagementFailed": "儲存配置管理失敗"
},
"version": {
"statusLabel": "版本狀態",
"notInstalled": "未安裝",
"remoteLocal": "遠端:{remoteVersion} · 本地:{localVersion}",
"platformUnsupported": "{versionText}。目前平台不支援該 Agent。",
"clickInstall": "{versionText}。請點擊右側安裝。",
"localUnrecognized": "{versionText}。本地版本無法識別,可嘗試升級覆蓋安裝。",
"upgradeAvailable": "{versionText}。發現可升級版本。",
"remoteUnavailable": "{versionText}。遠端版本暫不可用。",
"latest": "{versionText}。已是最新版本。"
}
},
"SettingsPages": {
"agentsLoading": "載入 Agent 設定中..."
}

View File

@@ -13,65 +13,41 @@ export type ShortcutActionId =
export interface ShortcutDefinition {
id: ShortcutActionId
title: string
description: string
}
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
id: "toggle_search",
title: "打开搜索",
description: "打开或关闭会话搜索面板",
},
{
id: "toggle_sidebar",
title: "切换左侧边栏",
description: "显示或隐藏会话列表侧边栏",
},
{
id: "toggle_terminal",
title: "切换终端",
description: "显示或隐藏底部终端面板",
},
{
id: "new_terminal_tab",
title: "新建终端",
description: "当最近鼠标活动在终端时新建终端标签",
},
{
id: "close_current_terminal_tab",
title: "关闭当前终端",
description: "当最近鼠标活动在终端时关闭当前终端标签",
},
{
id: "toggle_aux_panel",
title: "切换右侧面板",
description: "显示或隐藏辅助信息面板",
},
{
id: "new_conversation",
title: "新建会话",
description: "在当前文件夹中创建新的对话标签",
},
{
id: "open_folder",
title: "Open Folder",
description: "打开文件夹选择器并在新窗口中打开",
},
{
id: "open_settings",
title: "打开设置",
description: "打开设置窗口",
},
{
id: "close_current_tab",
title: "关闭当前标签",
description: "关闭当前会话或文件标签",
},
{
id: "close_all_file_tabs",
title: "关闭全部文件标签",
description: "仅在文件模式下关闭所有文件标签",
},
]