继续多语言补充

This commit is contained in:
xintaofei
2026-03-07 14:22:18 +08:00
parent 47189318e5
commit 89c91ac1eb
12 changed files with 398 additions and 117 deletions

View File

@@ -2,6 +2,7 @@
import { Suspense, useCallback, useEffect, useState } from "react" import { Suspense, useCallback, useEffect, useState } from "react"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useTranslations } from "next-intl"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { CommitWorkspace } from "@/components/layout/commit-dialog" import { CommitWorkspace } from "@/components/layout/commit-dialog"
@@ -19,6 +20,7 @@ interface FolderLoadState {
} }
function CommitPageInner() { function CommitPageInner() {
const t = useTranslations("CommitPage")
const searchParams = useSearchParams() const searchParams = useSearchParams()
const [state, setState] = useState<FolderLoadState>({ const [state, setState] = useState<FolderLoadState>({
loadedId: null, loadedId: null,
@@ -76,7 +78,8 @@ function CommitPageInner() {
<AppTitleBar <AppTitleBar
center={ center={
<div className="text-sm font-semibold tracking-tight"> <div className="text-sm font-semibold tracking-tight">
Git Commit{hasValidFolderId && folder ? ` · ${folder.name}` : ""} {t("title")}
{hasValidFolderId && folder ? ` · ${folder.name}` : ""}
</div> </div>
} }
/> />
@@ -84,12 +87,12 @@ function CommitPageInner() {
<main className="flex-1 min-h-0 p-3"> <main className="flex-1 min-h-0 p-3">
{!hasValidFolderId ? ( {!hasValidFolderId ? (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"> <div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
folderId {t("invalidFolderId")}
</div> </div>
) : loading ? ( ) : loading ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground"> <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
... {t("loadingRepo")}
</div> </div>
) : error ? ( ) : error ? (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"> <div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">

View File

@@ -12,6 +12,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useTranslations } from "next-intl"
import { cjk } from "@streamdown/cjk" import { cjk } from "@streamdown/cjk"
import { code } from "@streamdown/code" import { code } from "@streamdown/code"
import { math } from "@streamdown/math" import { math } from "@streamdown/math"
@@ -258,11 +259,12 @@ export const MessageBranchPrevious = ({
children, children,
...props ...props
}: MessageBranchPreviousProps) => { }: MessageBranchPreviousProps) => {
const t = useTranslations("Folder.chat.messageBranch")
const { goToPrevious, totalBranches } = useMessageBranch() const { goToPrevious, totalBranches } = useMessageBranch()
return ( return (
<Button <Button
aria-label="Previous branch" aria-label={t("previousBranchAria")}
disabled={totalBranches <= 1} disabled={totalBranches <= 1}
onClick={goToPrevious} onClick={goToPrevious}
size="icon-sm" size="icon-sm"
@@ -281,11 +283,12 @@ export const MessageBranchNext = ({
children, children,
...props ...props
}: MessageBranchNextProps) => { }: MessageBranchNextProps) => {
const t = useTranslations("Folder.chat.messageBranch")
const { goToNext, totalBranches } = useMessageBranch() const { goToNext, totalBranches } = useMessageBranch()
return ( return (
<Button <Button
aria-label="Next branch" aria-label={t("nextBranchAria")}
disabled={totalBranches <= 1} disabled={totalBranches <= 1}
onClick={goToNext} onClick={goToNext}
size="icon-sm" size="icon-sm"
@@ -304,6 +307,7 @@ export const MessageBranchPage = ({
className, className,
...props ...props
}: MessageBranchPageProps) => { }: MessageBranchPageProps) => {
const t = useTranslations("Folder.chat.messageBranch")
const { currentBranch, totalBranches } = useMessageBranch() const { currentBranch, totalBranches } = useMessageBranch()
return ( return (
@@ -314,7 +318,7 @@ export const MessageBranchPage = ({
)} )}
{...props} {...props}
> >
{currentBranch + 1} of {totalBranches} {t("pageOf", { current: currentBranch + 1, total: totalBranches })}
</ButtonGroupText> </ButtonGroupText>
) )
} }

View File

@@ -3,6 +3,7 @@
import type { ComponentProps, ReactNode } from "react" import type { ComponentProps, ReactNode } from "react"
import { useControllableState } from "@radix-ui/react-use-controllable-state" import { useControllableState } from "@radix-ui/react-use-controllable-state"
import { useTranslations } from "next-intl"
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@@ -155,24 +156,29 @@ export type ReasoningTriggerProps = ComponentProps<
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
} }
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) {
return <Shimmer duration={1}>Thinking...</Shimmer>
}
if (duration === undefined) {
return <p>Thought for a few seconds</p>
}
return <p>Thought for {duration} seconds</p>
}
export const ReasoningTrigger = memo( export const ReasoningTrigger = memo(
({ ({
className, className,
children, children,
getThinkingMessage = defaultGetThinkingMessage, getThinkingMessage,
...props ...props
}: ReasoningTriggerProps) => { }: ReasoningTriggerProps) => {
const t = useTranslations("Folder.chat.reasoning")
const { isStreaming, isOpen, duration } = useReasoning() const { isStreaming, isOpen, duration } = useReasoning()
const defaultGetThinkingMessage = useCallback(
(nextIsStreaming: boolean, nextDuration?: number) => {
if (nextIsStreaming || nextDuration === 0) {
return <Shimmer duration={1}>{t("thinking")}</Shimmer>
}
if (nextDuration === undefined) {
return <p>{t("thoughtForFewSeconds")}</p>
}
return <p>{t("thoughtForSeconds", { duration: nextDuration })}</p>
},
[t]
)
const thinkingMessageBuilder =
getThinkingMessage ?? defaultGetThinkingMessage
return ( return (
<CollapsibleTrigger <CollapsibleTrigger
@@ -185,7 +191,7 @@ export const ReasoningTrigger = memo(
{children ?? ( {children ?? (
<> <>
<BrainIcon className="size-4" /> <BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)} {thinkingMessageBuilder(isStreaming, duration)}
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"size-4 transition-transform", "size-4 transition-transform",

View File

@@ -3,6 +3,7 @@
import type { ComponentProps, HTMLAttributes } from "react" import type { ComponentProps, HTMLAttributes } from "react"
import Ansi from "ansi-to-react" import Ansi from "ansi-to-react"
import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react" import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react"
import { useTranslations } from "next-intl"
import { import {
createContext, createContext,
useCallback, useCallback,
@@ -127,6 +128,7 @@ export function TerminalTitle({
children, children,
...props ...props
}: TerminalTitleProps) { }: TerminalTitleProps) {
const t = useTranslations("Folder.chat.terminal")
return ( return (
<div <div
className={cn( className={cn(
@@ -136,7 +138,7 @@ export function TerminalTitle({
{...props} {...props}
> >
<TerminalIcon className="size-4" /> <TerminalIcon className="size-4" />
{children ?? "Terminal"} {children ?? t("title")}
</div> </div>
) )
} }
@@ -148,6 +150,7 @@ export function TerminalStatus({
children, children,
...props ...props
}: TerminalStatusProps) { }: TerminalStatusProps) {
const t = useTranslations("Folder.chat.terminal")
const { isStreaming } = useContext(TerminalContext) const { isStreaming } = useContext(TerminalContext)
if (!isStreaming) { if (!isStreaming) {
@@ -162,7 +165,7 @@ export function TerminalStatus({
)} )}
{...props} {...props}
> >
{children ?? <Shimmer>Running</Shimmer>} {children ?? <Shimmer>{t("running")}</Shimmer>}
</div> </div>
) )
} }

View File

@@ -18,6 +18,7 @@ import {
WrenchIcon, WrenchIcon,
XCircleIcon, XCircleIcon,
} from "lucide-react" } from "lucide-react"
import { useTranslations } from "next-intl"
import { isValidElement } from "react" import { isValidElement } from "react"
import { CodeBlock } from "./code-block" import { CodeBlock } from "./code-block"
@@ -47,16 +48,6 @@ export type ToolHeaderProps = {
} }
) )
const statusLabels: Record<ToolPart["state"], string> = {
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"input-available": "Running",
"input-streaming": "Pending",
"output-available": "Completed",
"output-denied": "Denied",
"output-error": "Error",
}
const statusIcons: Record<ToolPart["state"], ReactNode> = { const statusIcons: Record<ToolPart["state"], ReactNode> = {
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />, "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />, "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
@@ -67,10 +58,10 @@ const statusIcons: Record<ToolPart["state"], ReactNode> = {
"output-error": <XCircleIcon className="size-4 text-red-600" />, "output-error": <XCircleIcon className="size-4 text-red-600" />,
} }
export const getStatusBadge = (status: ToolPart["state"]) => ( export const getStatusBadge = (status: ToolPart["state"], label: string) => (
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary"> <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
{statusIcons[status]} {statusIcons[status]}
{statusLabels[status]} {label}
</Badge> </Badge>
) )
@@ -84,8 +75,23 @@ export const ToolHeader = ({
toolName, toolName,
...props ...props
}: ToolHeaderProps) => { }: ToolHeaderProps) => {
const t = useTranslations("Folder.chat.tool")
const derivedName = const derivedName =
type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-") type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-")
const statusLabel =
state === "approval-requested"
? t("status.approvalRequested")
: state === "approval-responded"
? t("status.approvalResponded")
: state === "input-available"
? t("status.inputAvailable")
: state === "input-streaming"
? t("status.inputStreaming")
: state === "output-available"
? t("status.outputAvailable")
: state === "output-denied"
? t("status.outputDenied")
: t("status.outputError")
return ( return (
<CollapsibleTrigger <CollapsibleTrigger
@@ -103,7 +109,7 @@ export const ToolHeader = ({
{title ?? derivedName} {title ?? derivedName}
</span> </span>
{titleSuffix ? <span className="shrink-0">{titleSuffix}</span> : null} {titleSuffix ? <span className="shrink-0">{titleSuffix}</span> : null}
<span className="shrink-0">{getStatusBadge(state)}</span> <span className="shrink-0">{getStatusBadge(state, statusLabel)}</span>
</div> </div>
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" /> <ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger> </CollapsibleTrigger>
@@ -127,6 +133,7 @@ export type ToolInputProps = ComponentProps<"div"> & {
} }
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => { export const ToolInput = ({ className, input, ...props }: ToolInputProps) => {
const t = useTranslations("Folder.chat.tool")
const formattedCode = (() => { const formattedCode = (() => {
if (typeof input === "string") { if (typeof input === "string") {
try { try {
@@ -142,7 +149,7 @@ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => {
return ( return (
<div className={cn("space-y-2 overflow-hidden", className)} {...props}> <div className={cn("space-y-2 overflow-hidden", className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Parameters {t("parameters")}
</h4> </h4>
<div className="rounded-md bg-muted/50"> <div className="rounded-md bg-muted/50">
<CodeBlock code={formattedCode} language="json" /> <CodeBlock code={formattedCode} language="json" />

View File

@@ -3,6 +3,7 @@ import type { BundledLanguage } from "shiki"
import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter" import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter"
import type { MessageRole } from "@/lib/types" import type { MessageRole } from "@/lib/types"
import { normalizeToolName } from "@/lib/tool-call-normalization" import { normalizeToolName } from "@/lib/tool-call-normalization"
import { useTranslations } from "next-intl"
import { import {
countUnifiedDiffLineChanges, countUnifiedDiffLineChanges,
estimateChangedLineStats, estimateChangedLineStats,
@@ -1881,6 +1882,7 @@ const ToolCallPart = memo(function ToolCallPart({
}: { }: {
part: Extract<AdaptedContentPart, { type: "tool-call" }> part: Extract<AdaptedContentPart, { type: "tool-call" }>
}) { }) {
const t = useTranslations("Folder.chat.contentParts")
const [manualOpen, setManualOpen] = useState(false) const [manualOpen, setManualOpen] = useState(false)
const normalizedToolName = useMemo( const normalizedToolName = useMemo(
() => normalizeToolName(part.toolName), () => normalizeToolName(part.toolName),
@@ -2046,7 +2048,7 @@ const ToolCallPart = memo(function ToolCallPart({
/> />
{liveOutputTruncated && ( {liveOutputTruncated && (
<div className="text-[11px] text-muted-foreground"> <div className="text-[11px] text-muted-foreground">
Showing tail output while streaming for performance. {t("showingTailOutput")}
</div> </div>
)} )}
</div> </div>
@@ -2068,9 +2070,14 @@ const ToolResultPart = memo(function ToolResultPart({
}: { }: {
part: Extract<AdaptedContentPart, { type: "tool-result" }> part: Extract<AdaptedContentPart, { type: "tool-result" }>
}) { }) {
const t = useTranslations("Folder.chat.contentParts")
return ( return (
<Tool> <Tool>
<ToolHeader type="dynamic-tool" state={part.state} toolName="Result" /> <ToolHeader
type="dynamic-tool"
state={part.state}
toolName={t("result")}
/>
<ToolContent> <ToolContent>
<ToolOutput output={part.output} errorText={part.errorText} /> <ToolOutput output={part.output} errorText={part.errorText} />
</ToolContent> </ToolContent>

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useLocale, useTranslations } from "next-intl"
import type { LiveMessage } from "@/contexts/acp-connections-context" import type { LiveMessage } from "@/contexts/acp-connections-context"
import { inferLiveToolName } from "@/lib/tool-call-normalization" import { inferLiveToolName } from "@/lib/tool-call-normalization"
import { import {
@@ -9,11 +10,6 @@ import {
} from "@/lib/line-change-stats" } from "@/lib/line-change-stats"
import { FilePenLine, Timer, Wrench } from "lucide-react" import { FilePenLine, Timer, Wrench } from "lucide-react"
function formatElapsed(ms: number): string {
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`
return `${(ms / 1_000).toFixed(1)}s`
}
interface LiveTurnStatsProps { interface LiveTurnStatsProps {
message: LiveMessage message: LiveMessage
} }
@@ -27,14 +23,9 @@ interface LiveEditStats extends LineChangeStats {
files: number files: number
} }
const COMPACT_NUMBER = new Intl.NumberFormat("en-US", { function formatCompactInt(n: number, formatter: Intl.NumberFormat): string {
notation: "compact",
maximumFractionDigits: 1,
})
function formatCompactInt(n: number): string {
if (n < 1000) return String(n) if (n < 1000) return String(n)
return COMPACT_NUMBER.format(n).toUpperCase() return formatter.format(n)
} }
function asObject(value: unknown): Record<string, unknown> | null { function asObject(value: unknown): Record<string, unknown> | null {
@@ -267,8 +258,18 @@ function extractLiveEditStats(message: LiveMessage): LiveEditStats {
} }
export function LiveTurnStats({ message }: LiveTurnStatsProps) { export function LiveTurnStats({ message }: LiveTurnStatsProps) {
const locale = useLocale()
const t = useTranslations("Folder.chat.liveTurnStats")
const [elapsed, setElapsed] = useState(() => Date.now() - message.startedAt) const [elapsed, setElapsed] = useState(() => Date.now() - message.startedAt)
const editStats = useMemo(() => extractLiveEditStats(message), [message]) const editStats = useMemo(() => extractLiveEditStats(message), [message])
const compactNumberFormatter = useMemo(
() =>
new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: 1,
}),
[locale]
)
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
@@ -295,26 +296,32 @@ export function LiveTurnStats({ message }: LiveTurnStatsProps) {
isThinking = true isThinking = true
} }
const elapsedLabel =
elapsed >= 60_000
? t("elapsedMinutes", { value: (elapsed / 60_000).toFixed(1) })
: t("elapsedSeconds", { value: (elapsed / 1_000).toFixed(1) })
return ( return (
<div className="flex h-8 shrink-0 items-center justify-center gap-3 px-4 text-xs leading-none text-muted-foreground"> <div className="flex h-8 shrink-0 items-center justify-center gap-3 px-4 text-xs leading-none text-muted-foreground">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-primary animate-pulse shrink-0" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-primary animate-pulse shrink-0" />
{isThinking && message.content.length <= 1 ? ( {isThinking && message.content.length <= 1 ? (
<span>Thinking...</span> <span>{t("thinking")}</span>
) : ( ) : (
<span>Streaming</span> <span>{t("streaming")}</span>
)} )}
<span className="text-border leading-none">|</span> <span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none"> <span className="inline-flex items-center gap-1 leading-none">
<Timer className="h-3 w-3 shrink-0" /> <Timer className="h-3 w-3 shrink-0" />
{formatElapsed(elapsed)} {elapsedLabel}
</span> </span>
{editStats.files > 0 && ( {editStats.files > 0 && (
<> <>
<span className="text-border leading-none">|</span> <span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none"> <span className="inline-flex items-center gap-1 leading-none">
<FilePenLine className="h-3 w-3 shrink-0" /> <FilePenLine className="h-3 w-3 shrink-0" />
{editStats.files}F +{formatCompactInt(editStats.additions)}/- {editStats.files}F +
{formatCompactInt(editStats.deletions)} {formatCompactInt(editStats.additions, compactNumberFormatter)}/-
{formatCompactInt(editStats.deletions, compactNumberFormatter)}
</span> </span>
</> </>
)} )}
@@ -323,7 +330,7 @@ export function LiveTurnStats({ message }: LiveTurnStatsProps) {
<span className="text-border leading-none">|</span> <span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none"> <span className="inline-flex items-center gap-1 leading-none">
<Wrench className="h-3 w-3 shrink-0" /> <Wrench className="h-3 w-3 shrink-0" />
{toolCallCount} tool {toolCallCount === 1 ? "use" : "uses"} {t("toolUseCount", { count: toolCallCount })}
</span> </span>
</> </>
)} )}

View File

@@ -25,6 +25,7 @@ import {
} from "@/components/ai-elements/message-thread" } from "@/components/ai-elements/message-thread"
import { Message, MessageContent } from "@/components/ai-elements/message" import { Message, MessageContent } from "@/components/ai-elements/message"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import { import {
buildPlanKey, buildPlanKey,
extractLatestPlanEntriesFromMessages, extractLatestPlanEntriesFromMessages,
@@ -46,7 +47,10 @@ interface ResolvedMessageGroup extends MessageGroup {
resources: UserResourceDisplay[] resources: UserResourceDisplay[]
} }
function fallbackExtractUserResources(group: MessageGroup): { function fallbackExtractUserResources(
group: MessageGroup,
attachedResourcesText: string
): {
parts: AdaptedContentPart[] parts: AdaptedContentPart[]
resources: UserResourceDisplay[] resources: UserResourceDisplay[]
} { } {
@@ -87,14 +91,17 @@ function fallbackExtractUserResources(group: MessageGroup): {
} }
if (parsedParts.length === 0 && dedupedResources.length > 0) { if (parsedParts.length === 0 && dedupedResources.length > 0) {
parsedParts.push({ type: "text", text: "Attached resources" }) parsedParts.push({ type: "text", text: attachedResourcesText })
} }
return { parts: parsedParts, resources: dedupedResources } return { parts: parsedParts, resources: dedupedResources }
} }
function resolveMessageGroup(group: MessageGroup): ResolvedMessageGroup { function resolveMessageGroup(
const resolved = fallbackExtractUserResources(group) group: MessageGroup,
attachedResourcesText: string
): ResolvedMessageGroup {
const resolved = fallbackExtractUserResources(group, attachedResourcesText)
return { return {
...group, ...group,
parts: resolved.parts, parts: resolved.parts,
@@ -161,6 +168,7 @@ export function MessageListView({
onPendingClear, onPendingClear,
isActive = true, isActive = true,
}: MessageListViewProps) { }: MessageListViewProps) {
const t = useTranslations("Folder.chat.messageList")
const { detail, loading, error, refetch } = useDbMessageDetail(conversationId) const { detail, loading, error, refetch } = useDbMessageDetail(conversationId)
const turnCount = detail?.turns.length ?? 0 const turnCount = detail?.turns.length ?? 0
@@ -225,12 +233,16 @@ export function MessageListView({
[pendingMessages] [pendingMessages]
) )
const resolvedGroups = useMemo( const resolvedGroups = useMemo(
() => groups.map(resolveMessageGroup), () =>
[groups] groups.map((group) => resolveMessageGroup(group, t("attachedResources"))),
[groups, t]
) )
const resolvedPendingGroups = useMemo( const resolvedPendingGroups = useMemo(
() => pendingGroups.map(resolveMessageGroup), () =>
[pendingGroups] pendingGroups.map((group) =>
resolveMessageGroup(group, t("attachedResources"))
),
[pendingGroups, t]
) )
const showLiveMessage = Boolean( const showLiveMessage = Boolean(
@@ -245,7 +257,7 @@ export function MessageListView({
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span> <span>{t("loading")}</span>
</div> </div>
</div> </div>
) )
@@ -255,7 +267,9 @@ export function MessageListView({
return ( return (
<div className="p-6"> <div className="p-6">
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-destructive text-sm">Error: {error}</p> <p className="text-destructive text-sm">
{t("error", { message: error })}
</p>
</div> </div>
</div> </div>
) )
@@ -276,7 +290,7 @@ export function MessageListView({
!showLiveMessage ? ( !showLiveMessage ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
No messages in this conversation. {t("emptyConversation")}
</p> </p>
</div> </div>
) : ( ) : (

View File

@@ -10,6 +10,7 @@ import {
useState, useState,
type ReactNode, type ReactNode,
} from "react" } from "react"
import { useTranslations } from "next-intl"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { import {
gitDiff, gitDiff,
@@ -172,6 +173,7 @@ interface WorkspaceProviderProps {
} }
export function WorkspaceProvider({ children }: WorkspaceProviderProps) { export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const t = useTranslations("Folder.workspaceContext")
const { folder, folderId } = useFolderContext() const { folder, folderId } = useFolderContext()
const folderPath = folder?.path const folderPath = folder?.path
const storageKey = useMemo( const storageKey = useMemo(
@@ -270,7 +272,11 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const rejectTab = useCallback( const rejectTab = useCallback(
(tabId: string, errorMessage: string) => { (tabId: string, errorMessage: string) => {
resolveTab(tabId, `Unable to load content.\n\n${errorMessage}`, false) resolveTab(
tabId,
t("unableLoadContent", { message: errorMessage }),
false
)
setFileTabs((prev) => setFileTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === tabId tab.id === tabId
@@ -283,7 +289,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
) )
) )
}, },
[resolveTab] [resolveTab, t]
) )
const resolveRichDiffTab = useCallback( const resolveRichDiffTab = useCallback(
@@ -333,7 +339,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
})(), })(),
]), ]),
15_000, 15_000,
"Preview request timed out" t("previewRequestTimedOut")
) )
setFileTabs((prev) => setFileTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
@@ -360,7 +366,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
rejectTab(tabId, error instanceof Error ? error.message : String(error)) rejectTab(tabId, error instanceof Error ? error.message : String(error))
} }
}, },
[folderPath, rejectTab, upsertLoadingTab] [folderPath, rejectTab, t, upsertLoadingTab]
) )
const openWorkingTreeDiff = useCallback( const openWorkingTreeDiff = useCallback(
@@ -372,8 +378,8 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
if (!rawPath) { if (!rawPath) {
const tabId = "diff:working:all" const tabId = "diff:working:all"
const title = "Diff · Workspace" const title = t("diffTitleWorkspace")
const description = "Working tree (HEAD)" const description = t("diffDescriptionWorkingTree")
upsertLoadingTab( upsertLoadingTab(
loadingTab(tabId, "diff", title, description, null, "diff") loadingTab(tabId, "diff", title, description, null, "diff")
) )
@@ -381,9 +387,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const result = await withTimeout( const result = await withTimeout(
gitDiff(folderPath), gitDiff(folderPath),
20_000, 20_000,
"Diff request timed out" t("diffRequestTimedOut")
) )
resolveTab(tabId, result || "No changes.", false) resolveTab(tabId, result || t("noChanges"), false)
} catch (error) { } catch (error) {
rejectTab( rejectTab(
tabId, tabId,
@@ -399,7 +405,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
if (mode === "overview") { if (mode === "overview") {
const encodedPath = encodeURIComponent(path) const encodedPath = encodeURIComponent(path)
const tabId = `diff:working-overview:${encodedPath}` const tabId = `diff:working-overview:${encodedPath}`
const title = `Diff · ${fileName(path)}` const title = t("diffTitleFile", { name: fileName(path) })
const description = path const description = path
upsertLoadingTab( upsertLoadingTab(
loadingTab(tabId, "diff", title, description, path, "diff") loadingTab(tabId, "diff", title, description, path, "diff")
@@ -409,9 +415,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const result = await withTimeout( const result = await withTimeout(
gitDiff(folderPath, path), gitDiff(folderPath, path),
20_000, 20_000,
"Diff request timed out" t("diffRequestTimedOut")
) )
resolveTab(tabId, result || "No changes.", false) resolveTab(tabId, result || t("noChanges"), false)
} catch (error) { } catch (error) {
rejectTab( rejectTab(
tabId, tabId,
@@ -423,7 +429,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
if (mode === "unified") { if (mode === "unified") {
const tabId = `diff:working:${path}:unified` const tabId = `diff:working:${path}:unified`
const title = `Diff · ${fileName(path)}` const title = t("diffTitleFile", { name: fileName(path) })
const description = path const description = path
upsertLoadingTab( upsertLoadingTab(
loadingTab(tabId, "diff", title, description, path, "diff") loadingTab(tabId, "diff", title, description, path, "diff")
@@ -433,9 +439,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const result = await withTimeout( const result = await withTimeout(
gitDiff(folderPath, path), gitDiff(folderPath, path),
20_000, 20_000,
"Diff request timed out" t("diffRequestTimedOut")
) )
resolveTab(tabId, result || "No changes.", false) resolveTab(tabId, result || t("noChanges"), false)
} catch (error) { } catch (error) {
rejectTab( rejectTab(
tabId, tabId,
@@ -446,7 +452,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
} }
const tabId = `diff:working:${path}` const tabId = `diff:working:${path}`
const title = `Diff · ${fileName(path)}` const title = t("diffTitleFile", { name: fileName(path) })
const description = path const description = path
const lang = languageFromPath(path) const lang = languageFromPath(path)
@@ -465,14 +471,14 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
})), })),
]), ]),
20_000, 20_000,
"Diff request timed out" t("diffRequestTimedOut")
) )
resolveRichDiffTab(tabId, originalContent, modifiedResult.content) resolveRichDiffTab(tabId, originalContent, modifiedResult.content)
} catch (error) { } catch (error) {
rejectTab(tabId, error instanceof Error ? error.message : String(error)) rejectTab(tabId, error instanceof Error ? error.message : String(error))
} }
}, },
[folderPath, rejectTab, resolveTab, resolveRichDiffTab, upsertLoadingTab] [folderPath, rejectTab, resolveTab, resolveRichDiffTab, t, upsertLoadingTab]
) )
const openBranchDiff = useCallback( const openBranchDiff = useCallback(
@@ -494,11 +500,11 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
? `diff:branch-overview:${encodedBranch}:${encodedPath}` ? `diff:branch-overview:${encodedBranch}:${encodedPath}`
: `diff:branch:${targetBranch}:${path ?? "all"}` : `diff:branch:${targetBranch}:${path ?? "all"}`
const title = path const title = path
? `Compare · ${fileName(path)}` ? t("compareTitleFile", { name: fileName(path) })
: `Compare · ${targetBranch}` : t("compareTitleBranch", { branch: targetBranch })
const description = path const description = path
? `${path} · compare with ${targetBranch}` ? t("compareDescriptionPath", { path, branch: targetBranch })
: `compare with ${targetBranch}` : t("compareDescriptionBranch", { branch: targetBranch })
if (mode !== "overview" && path) { if (mode !== "overview" && path) {
const lang = languageFromPath(path) const lang = languageFromPath(path)
@@ -517,7 +523,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
})), })),
]), ]),
20_000, 20_000,
"Branch compare request timed out" t("branchCompareRequestTimedOut")
) )
resolveRichDiffTab(tabId, originalContent, modifiedResult.content) resolveRichDiffTab(tabId, originalContent, modifiedResult.content)
} catch (error) { } catch (error) {
@@ -537,14 +543,14 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const result = await withTimeout( const result = await withTimeout(
gitDiffWithBranch(folderPath, targetBranch, path ?? undefined), gitDiffWithBranch(folderPath, targetBranch, path ?? undefined),
20_000, 20_000,
"Branch compare request timed out" t("branchCompareRequestTimedOut")
) )
resolveTab(tabId, result || "No changes.", false) resolveTab(tabId, result || t("noChanges"), false)
} catch (error) { } catch (error) {
rejectTab(tabId, error instanceof Error ? error.message : String(error)) rejectTab(tabId, error instanceof Error ? error.message : String(error))
} }
}, },
[folderPath, rejectTab, resolveRichDiffTab, resolveTab, upsertLoadingTab] [folderPath, rejectTab, resolveRichDiffTab, resolveTab, t, upsertLoadingTab]
) )
const openCommitDiff = useCallback( const openCommitDiff = useCallback(
@@ -553,11 +559,14 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const path = rawPath ? normalizePath(rawPath) : null const path = rawPath ? normalizePath(rawPath) : null
const tabId = `diff:commit:${commit}:${path ?? "all"}` const tabId = `diff:commit:${commit}:${path ?? "all"}`
const title = path const title = path
? `Diff · ${fileName(path)} @ ${commit.slice(0, 7)}` ? t("diffTitleCommitFile", {
: `Diff · ${commit.slice(0, 7)}` name: fileName(path),
hash: commit.slice(0, 7),
})
: t("diffTitleCommit", { hash: commit.slice(0, 7) })
const description = path const description = path
? `${path} · commit ${commit}` ? t("diffDescriptionCommitPath", { path, commit })
: message || `commit ${commit}` : message || t("diffDescriptionCommit", { commit })
if (path) { if (path) {
const lang = languageFromPath(path) const lang = languageFromPath(path)
@@ -572,7 +581,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
gitShowFile(folderPath, path, commit).catch(() => ""), gitShowFile(folderPath, path, commit).catch(() => ""),
]), ]),
20_000, 20_000,
"Commit diff request timed out" t("commitDiffRequestTimedOut")
) )
resolveRichDiffTab(tabId, originalContent, modifiedContent) resolveRichDiffTab(tabId, originalContent, modifiedContent)
} catch (error) { } catch (error) {
@@ -590,9 +599,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const result = await withTimeout( const result = await withTimeout(
gitShowDiff(folderPath, commit, undefined), gitShowDiff(folderPath, commit, undefined),
20_000, 20_000,
"Commit diff request timed out" t("commitDiffRequestTimedOut")
) )
resolveTab(tabId, result || "No diff output.", false) resolveTab(tabId, result || t("noDiffOutput"), false)
} catch (error) { } catch (error) {
rejectTab( rejectTab(
tabId, tabId,
@@ -601,14 +610,14 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
} }
} }
}, },
[folderPath, rejectTab, resolveTab, resolveRichDiffTab, upsertLoadingTab] [folderPath, rejectTab, resolveTab, resolveRichDiffTab, t, upsertLoadingTab]
) )
const openSessionFileDiff = useCallback( const openSessionFileDiff = useCallback(
(filePath: string, diffContent: string, groupLabel: string) => { (filePath: string, diffContent: string, groupLabel: string) => {
const path = normalizePath(filePath) const path = normalizePath(filePath)
const tabId = `diff:session:${groupLabel}:${path}` const tabId = `diff:session:${groupLabel}:${path}`
const title = `Diff · ${fileName(path)}` const title = t("diffTitleFile", { name: fileName(path) })
const description = `${path} · ${groupLabel}` const description = `${path} · ${groupLabel}`
const tab: FileWorkspaceTab = { const tab: FileWorkspaceTab = {
@@ -624,15 +633,15 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
upsertLoadingTab(tab) upsertLoadingTab(tab)
}, },
[upsertLoadingTab] [t, upsertLoadingTab]
) )
const openExternalConflictDiff = useCallback( const openExternalConflictDiff = useCallback(
(filePath: string, diskContent: string, unsavedContent: string) => { (filePath: string, diskContent: string, unsavedContent: string) => {
const path = normalizePath(filePath) const path = normalizePath(filePath)
const tabId = `diff:external-conflict:${path}` const tabId = `diff:external-conflict:${path}`
const title = `Conflict · ${fileName(path)}` const title = t("diffTitleConflictFile", { name: fileName(path) })
const description = `${path} · disk vs unsaved` const description = t("diffDescriptionConflict", { path })
const language = languageFromPath(path) const language = languageFromPath(path)
const tab: FileWorkspaceTab = { const tab: FileWorkspaceTab = {
@@ -650,7 +659,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
upsertLoadingTab(tab) upsertLoadingTab(tab)
}, },
[upsertLoadingTab] [t, upsertLoadingTab]
) )
const updateActiveFileContent = useCallback( const updateActiveFileContent = useCallback(
@@ -712,7 +721,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
expectedEtag expectedEtag
), ),
20_000, 20_000,
"Save request timed out" t("saveRequestTimedOut")
) )
setFileTabs((prev) => setFileTabs((prev) =>
@@ -753,7 +762,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
return false return false
} }
}, },
[folderPath] [folderPath, t]
) )
const saveActiveFile = useCallback( const saveActiveFile = useCallback(
@@ -799,7 +808,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
})(), })(),
]), ]),
15_000, 15_000,
"Reload request timed out" t("reloadRequestTimedOut")
) )
setFileTabs((prev) => setFileTabs((prev) =>
prev.map((candidate) => prev.map((candidate) =>
@@ -838,7 +847,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
) )
} }
}, },
[folderPath] [folderPath, t]
) )
const reloadActiveFile = useCallback(async () => { const reloadActiveFile = useCallback(async () => {
@@ -866,7 +875,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const tab = prev[idx] const tab = prev[idx]
if (isDirtyFileTab(tab)) { if (isDirtyFileTab(tab)) {
const confirmed = window.confirm( const confirmed = window.confirm(
`${tab.title}” 有未保存更改,确定关闭吗?` t("confirmCloseDirtyTab", { title: tab.title })
) )
if (!confirmed) return prev if (!confirmed) return prev
} }
@@ -886,7 +895,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
return next return next
}) })
}, },
[activateConversationPane] [activateConversationPane, t]
) )
const closeOtherFileTabs = useCallback( const closeOtherFileTabs = useCallback(
@@ -897,9 +906,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
const closingTabs = prev.filter((tab) => tab.id !== tabId) const closingTabs = prev.filter((tab) => tab.id !== tabId)
if (closingTabs.some(isDirtyFileTab)) { if (closingTabs.some(isDirtyFileTab)) {
const confirmed = window.confirm( const confirmed = window.confirm(t("confirmCloseOtherDirtyTabs"))
"存在未保存文件,确定关闭其它标签页吗?"
)
if (!confirmed) return prev if (!confirmed) return prev
} }
@@ -908,15 +915,13 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
return remaining return remaining
}) })
}, },
[activateFilePane] [activateFilePane, t]
) )
const closeAllFileTabs = useCallback(() => { const closeAllFileTabs = useCallback(() => {
setFileTabs((prev) => { setFileTabs((prev) => {
if (prev.some(isDirtyFileTab)) { if (prev.some(isDirtyFileTab)) {
const confirmed = window.confirm( const confirmed = window.confirm(t("confirmCloseAllDirtyTabs"))
"存在未保存文件,确定关闭全部标签页吗?"
)
if (!confirmed) return prev if (!confirmed) return prev
} }
@@ -924,7 +929,7 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
activateConversationPane() activateConversationPane()
return [] return []
}) })
}, [activateConversationPane]) }, [activateConversationPane, t])
const reorderFileTabs = useCallback((tabs: FileWorkspaceTab[]) => { const reorderFileTabs = useCallback((tabs: FileWorkspaceTab[]) => {
setFileTabs(tabs) setFileTabs(tabs)

View File

@@ -513,6 +513,11 @@
"SettingsPages": { "SettingsPages": {
"agentsLoading": "Loading agent settings..." "agentsLoading": "Loading agent settings..."
}, },
"CommitPage": {
"title": "Commit",
"invalidFolderId": "Invalid folder ID",
"loadingRepo": "Loading repository..."
},
"Folder": { "Folder": {
"common": { "common": {
"all": "All", "all": "All",
@@ -1023,6 +1028,33 @@
"saving": "Saving..." "saving": "Saving..."
} }
}, },
"workspaceContext": {
"confirmCloseDirtyTab": "Close \"{title}\" without saving?",
"confirmCloseOtherDirtyTabs": "Close other tabs with unsaved changes?",
"confirmCloseAllDirtyTabs": "Close all tabs with unsaved changes?",
"unableLoadContent": "Unable to load content.\n\n{message}",
"previewRequestTimedOut": "Preview request timed out",
"diffRequestTimedOut": "Diff request timed out",
"branchCompareRequestTimedOut": "Branch compare request timed out",
"commitDiffRequestTimedOut": "Commit diff request timed out",
"saveRequestTimedOut": "Save request timed out",
"reloadRequestTimedOut": "Reload request timed out",
"noChanges": "No changes.",
"noDiffOutput": "No diff output.",
"diffTitleWorkspace": "Diff · Workspace",
"diffDescriptionWorkingTree": "Working tree (HEAD)",
"diffTitleFile": "Diff · {name}",
"compareTitleFile": "Compare · {name}",
"compareTitleBranch": "Compare · {branch}",
"compareDescriptionPath": "{path} · compare with {branch}",
"compareDescriptionBranch": "compare with {branch}",
"diffTitleCommitFile": "Diff · {name} @ {hash}",
"diffTitleCommit": "Diff · {hash}",
"diffDescriptionCommitPath": "{path} · commit {commit}",
"diffDescriptionCommit": "commit {commit}",
"diffTitleConflictFile": "Conflict · {name}",
"diffDescriptionConflict": "{path} · disk vs unsaved"
},
"chat": { "chat": {
"chatInput": { "chatInput": {
"connecting": "Connecting...", "connecting": "Connecting...",
@@ -1078,6 +1110,49 @@
"moreFiles": "+{count} more files", "moreFiles": "+{count} more files",
"plan": "Plan", "plan": "Plan",
"targetMode": "Target mode: {mode}" "targetMode": "Target mode: {mode}"
},
"messageBranch": {
"previousBranchAria": "Previous branch",
"nextBranchAria": "Next branch",
"pageOf": "{current} of {total}"
},
"terminal": {
"title": "Terminal",
"running": "Running"
},
"reasoning": {
"thinking": "Thinking...",
"thoughtForFewSeconds": "Thought for a few seconds",
"thoughtForSeconds": "Thought for {duration} seconds"
},
"messageList": {
"attachedResources": "Attached resources",
"loading": "Loading...",
"error": "Error: {message}",
"emptyConversation": "No messages in this conversation."
},
"liveTurnStats": {
"thinking": "Thinking...",
"streaming": "Streaming",
"elapsedMinutes": "{value}m",
"elapsedSeconds": "{value}s",
"toolUseCount": "{count} tool {count, plural, one {use} other {uses}}"
},
"tool": {
"parameters": "Parameters",
"status": {
"approvalRequested": "Awaiting Approval",
"approvalResponded": "Responded",
"inputAvailable": "Running",
"inputStreaming": "Pending",
"outputAvailable": "Completed",
"outputDenied": "Denied",
"outputError": "Error"
}
},
"contentParts": {
"showingTailOutput": "Showing tail output while streaming for performance.",
"result": "Result"
} }
}, },
"diffPreview": { "diffPreview": {

View File

@@ -513,6 +513,11 @@
"SettingsPages": { "SettingsPages": {
"agentsLoading": "加载 Agent 设置中..." "agentsLoading": "加载 Agent 设置中..."
}, },
"CommitPage": {
"title": "提交代码",
"invalidFolderId": "无效的 folderId",
"loadingRepo": "正在加载仓库..."
},
"Folder": { "Folder": {
"common": { "common": {
"all": "全部", "all": "全部",
@@ -1023,6 +1028,33 @@
"saving": "保存中..." "saving": "保存中..."
} }
}, },
"workspaceContext": {
"confirmCloseDirtyTab": "文件“{title}”有未保存更改,确定关闭吗?",
"confirmCloseOtherDirtyTabs": "其它标签页有未保存更改,确定关闭吗?",
"confirmCloseAllDirtyTabs": "存在未保存更改,确定关闭全部标签页吗?",
"unableLoadContent": "无法加载内容。\n\n{message}",
"previewRequestTimedOut": "预览请求超时",
"diffRequestTimedOut": "Diff 请求超时",
"branchCompareRequestTimedOut": "分支比较请求超时",
"commitDiffRequestTimedOut": "提交差异请求超时",
"saveRequestTimedOut": "保存请求超时",
"reloadRequestTimedOut": "重载请求超时",
"noChanges": "暂无变更。",
"noDiffOutput": "无差异输出。",
"diffTitleWorkspace": "Diff · 工作区",
"diffDescriptionWorkingTree": "工作区变更HEAD",
"diffTitleFile": "Diff · {name}",
"compareTitleFile": "比较 · {name}",
"compareTitleBranch": "比较 · {branch}",
"compareDescriptionPath": "{path} · 与 {branch} 比较",
"compareDescriptionBranch": "与 {branch} 比较",
"diffTitleCommitFile": "Diff · {name} @ {hash}",
"diffTitleCommit": "Diff · {hash}",
"diffDescriptionCommitPath": "{path} · 提交 {commit}",
"diffDescriptionCommit": "提交 {commit}",
"diffTitleConflictFile": "冲突 · {name}",
"diffDescriptionConflict": "{path} · 磁盘与未保存内容"
},
"chat": { "chat": {
"chatInput": { "chatInput": {
"connecting": "连接中...", "connecting": "连接中...",
@@ -1078,6 +1110,49 @@
"moreFiles": "+{count} 个更多文件", "moreFiles": "+{count} 个更多文件",
"plan": "计划", "plan": "计划",
"targetMode": "目标模式:{mode}" "targetMode": "目标模式:{mode}"
},
"messageBranch": {
"previousBranchAria": "上一分支",
"nextBranchAria": "下一分支",
"pageOf": "{current} / {total}"
},
"terminal": {
"title": "终端",
"running": "运行中"
},
"reasoning": {
"thinking": "思考中...",
"thoughtForFewSeconds": "思考了几秒",
"thoughtForSeconds": "思考了 {duration} 秒"
},
"messageList": {
"attachedResources": "附加资源",
"loading": "加载中...",
"error": "错误:{message}",
"emptyConversation": "当前会话暂无消息。"
},
"liveTurnStats": {
"thinking": "思考中...",
"streaming": "生成中",
"elapsedMinutes": "{value} 分钟",
"elapsedSeconds": "{value} 秒",
"toolUseCount": "{count} 次工具调用"
},
"tool": {
"parameters": "参数",
"status": {
"approvalRequested": "等待授权",
"approvalResponded": "已响应",
"inputAvailable": "运行中",
"inputStreaming": "等待中",
"outputAvailable": "已完成",
"outputDenied": "已拒绝",
"outputError": "错误"
}
},
"contentParts": {
"showingTailOutput": "为保证性能,流式输出时仅显示尾部内容。",
"result": "结果"
} }
}, },
"diffPreview": { "diffPreview": {

View File

@@ -513,6 +513,11 @@
"SettingsPages": { "SettingsPages": {
"agentsLoading": "載入 Agent 設定中..." "agentsLoading": "載入 Agent 設定中..."
}, },
"CommitPage": {
"title": "提交程式碼",
"invalidFolderId": "無效的 folderId",
"loadingRepo": "正在載入倉庫..."
},
"Folder": { "Folder": {
"common": { "common": {
"all": "全部", "all": "全部",
@@ -1023,6 +1028,33 @@
"saving": "儲存中..." "saving": "儲存中..."
} }
}, },
"workspaceContext": {
"confirmCloseDirtyTab": "檔案「{title}」有未儲存變更,確定關閉嗎?",
"confirmCloseOtherDirtyTabs": "其他分頁有未儲存變更,確定關閉嗎?",
"confirmCloseAllDirtyTabs": "存在未儲存變更,確定關閉全部分頁嗎?",
"unableLoadContent": "無法載入內容。\n\n{message}",
"previewRequestTimedOut": "預覽請求逾時",
"diffRequestTimedOut": "Diff 請求逾時",
"branchCompareRequestTimedOut": "分支比較請求逾時",
"commitDiffRequestTimedOut": "提交差異請求逾時",
"saveRequestTimedOut": "儲存請求逾時",
"reloadRequestTimedOut": "重載請求逾時",
"noChanges": "暫無變更。",
"noDiffOutput": "無差異輸出。",
"diffTitleWorkspace": "Diff · 工作區",
"diffDescriptionWorkingTree": "工作區變更HEAD",
"diffTitleFile": "Diff · {name}",
"compareTitleFile": "比較 · {name}",
"compareTitleBranch": "比較 · {branch}",
"compareDescriptionPath": "{path} · 與 {branch} 比較",
"compareDescriptionBranch": "與 {branch} 比較",
"diffTitleCommitFile": "Diff · {name} @ {hash}",
"diffTitleCommit": "Diff · {hash}",
"diffDescriptionCommitPath": "{path} · 提交 {commit}",
"diffDescriptionCommit": "提交 {commit}",
"diffTitleConflictFile": "衝突 · {name}",
"diffDescriptionConflict": "{path} · 磁碟與未儲存內容"
},
"chat": { "chat": {
"chatInput": { "chatInput": {
"connecting": "連線中...", "connecting": "連線中...",
@@ -1078,6 +1110,49 @@
"moreFiles": "+{count} 個更多檔案", "moreFiles": "+{count} 個更多檔案",
"plan": "計畫", "plan": "計畫",
"targetMode": "目標模式:{mode}" "targetMode": "目標模式:{mode}"
},
"messageBranch": {
"previousBranchAria": "上一個分支",
"nextBranchAria": "下一個分支",
"pageOf": "{current} / {total}"
},
"terminal": {
"title": "終端",
"running": "執行中"
},
"reasoning": {
"thinking": "思考中...",
"thoughtForFewSeconds": "思考了幾秒",
"thoughtForSeconds": "思考了 {duration} 秒"
},
"messageList": {
"attachedResources": "附加資源",
"loading": "載入中...",
"error": "錯誤:{message}",
"emptyConversation": "目前會話暫無訊息。"
},
"liveTurnStats": {
"thinking": "思考中...",
"streaming": "生成中",
"elapsedMinutes": "{value} 分鐘",
"elapsedSeconds": "{value} 秒",
"toolUseCount": "{count} 次工具呼叫"
},
"tool": {
"parameters": "參數",
"status": {
"approvalRequested": "等待授權",
"approvalResponded": "已回應",
"inputAvailable": "執行中",
"inputStreaming": "等待中",
"outputAvailable": "已完成",
"outputDenied": "已拒絕",
"outputError": "錯誤"
}
},
"contentParts": {
"showingTailOutput": "為確保效能,串流輸出時僅顯示尾端內容。",
"result": "結果"
} }
}, },
"diffPreview": { "diffPreview": {