diff --git a/src/components/ai-elements/message-thread.tsx b/src/components/ai-elements/message-thread.tsx index b2c14aa..c4611da 100644 --- a/src/components/ai-elements/message-thread.tsx +++ b/src/components/ai-elements/message-thread.tsx @@ -5,6 +5,7 @@ import type { ComponentProps } from "react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { ArrowDownIcon, DownloadIcon } from "lucide-react" +import { useTranslations } from "next-intl" import { useCallback } from "react" import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom" @@ -42,32 +43,37 @@ export type MessageThreadEmptyStateProps = ComponentProps<"div"> & { export const MessageThreadEmptyState = ({ className, - title = "No messages yet", - description = "Start a conversation to see messages here", + title, + description, icon, children, ...props -}: MessageThreadEmptyStateProps) => ( -
- {children ?? ( - <> - {icon &&
{icon}
} -
-

{title}

- {description && ( -

{description}

- )} -
- - )} -
-) +}: MessageThreadEmptyStateProps) => { + const t = useTranslations("Folder.chat.messageThread") + return ( +
+ {children ?? ( + <> + {icon &&
{icon}
} +
+

{title ?? t("emptyTitle")}

+ {(description ?? t("emptyDescription")) && ( +

+ {description ?? t("emptyDescription")} +

+ )} +
+ + )} +
+ ) +} export type MessageThreadScrollButtonProps = ComponentProps diff --git a/src/components/ai-elements/tool.tsx b/src/components/ai-elements/tool.tsx index a439851..0d32a1a 100644 --- a/src/components/ai-elements/tool.tsx +++ b/src/components/ai-elements/tool.tsx @@ -323,6 +323,7 @@ export const ToolOutput = ({ errorText, ...props }: ToolOutputProps) => { + const t = useTranslations("Folder.chat.tool") if (!(output || errorText)) { return null } @@ -348,7 +349,7 @@ export const ToolOutput = ({ return (

- {errorText ? "Error" : "Result"} + {errorText ? t("error") : t("result")}

}) { + const t = useTranslations("Folder.chat.contentParts") const filePath = str(input, "file_path") const oldString = str(input, "old_string") ?? "" const newString = str(input, "new_string") ?? "" @@ -1109,11 +1110,11 @@ function EditToolInput({ input }: { input: Record }) {
- {filePath ?? "unknown"} + {filePath ?? t("unknown")} {replaceAll && ( - REPLACE ALL + {t("replaceAll")} )}
@@ -1124,6 +1125,7 @@ function EditToolInput({ input }: { input: Record }) { /** Edit tool (changes payload): file list + summary + combined diff view */ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) { + const t = useTranslations("Folder.chat.contentParts") const { additions, deletions, diffCode } = useMemo(() => { let additions = 0 let deletions = 0 @@ -1167,7 +1169,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) { return (
- Files: {changes.length} + {t("filesCount", { count: changes.length })} {additions > 0 && +{additions}} {deletions > 0 && -{deletions}}
@@ -1175,7 +1177,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) { {changes.slice(0, 8).map((change, index) => (
- update + {t("update")} {change.path} @@ -1184,7 +1186,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) { ))} {changes.length > 8 && (
- +{changes.length - 8} more files + {t("moreFiles", { count: changes.length - 8 })}
)}
@@ -1195,6 +1197,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) { /** Bash / exec_command: terminal-style command display */ function BashToolInput({ input }: { input: Record }) { + const t = useTranslations("Folder.chat.contentParts") const command = commandFromUnknownValue(input) ?? str(input, "command") ?? @@ -1216,8 +1219,8 @@ function BashToolInput({ input }: { input: Record }) { {displayCommand && } {(timeout || background) && (
- {timeout && Timeout: {timeout}ms} - {background && Background: true} + {timeout && {t("timeoutMs", { timeout })}} + {background && {t("backgroundTrue")}}
)}
@@ -1232,6 +1235,7 @@ function FileToolInput({ toolName: string input: Record }) { + const t = useTranslations("Folder.chat.contentParts") const name = toolName.toLowerCase() const filePath = str(input, "file_path") ?? str(input, "path") ?? str(input, "notebook_path") @@ -1263,15 +1267,15 @@ function FileToolInput({ )} {(offset != null || limit != null || pages) && (
- {offset != null && Offset: {offset}} - {limit != null && Limit: {limit}} - {pages && Pages: {pages}} + {offset != null && {t("offset", { offset })}} + {limit != null && {t("limit", { limit })}} + {pages && {t("pages", { pages })}}
)} {(cellType || editMode) && (
- {editMode && Mode: {editMode}} - {cellType && Cell: {cellType}} + {editMode && {t("mode", { mode: editMode })}} + {cellType && {t("cell", { cell: cellType })}}
)} {(name === "write" || name === "notebookedit") && @@ -1295,6 +1299,7 @@ function SearchToolInput({ toolName: string input: Record }) { + const t = useTranslations("Folder.chat.contentParts") const name = toolName.toLowerCase() const pattern = str(input, "pattern") const path = str(input, "path") @@ -1315,27 +1320,30 @@ function SearchToolInput({
{path && ( - Path: {path} + {t("pathLabel")}{" "} + {path} )} {glob && ( - Glob: {glob} + {t("globLabel")}{" "} + {glob} )} {fileType && ( - Type: {fileType} + {t("typeLabel")}{" "} + {fileType} )} {name === "grep" && outputMode && ( - Output:{" "} + {t("outputLabel")}{" "} {outputMode} )} - {caseInsensitive && Case insensitive} - {multiline && Multiline} + {caseInsensitive && {t("caseInsensitive")}} + {multiline && {t("multiline")}}
) @@ -1349,6 +1357,7 @@ function WebToolInput({ toolName: string input: Record }) { + const t = useTranslations("Folder.chat.contentParts") const name = toolName.toLowerCase() const url = str(input, "url") const query = str(input, "query") @@ -1375,7 +1384,7 @@ function WebToolInput({ {prompt && (
- Prompt + {t("promptLabel")}
{prompt} @@ -1388,6 +1397,7 @@ function WebToolInput({ /** Task tools: description / subject focused */ function TaskToolInput({ input }: { input: Record }) { + const t = useTranslations("Folder.chat.contentParts") const subject = str(input, "subject") const taskId = str(input, "taskId") const status = str(input, "status") @@ -1401,7 +1411,7 @@ function TaskToolInput({ input }: { input: Record }) { {subject && (
- Subject + {t("subjectLabel")} {subject}
@@ -1409,7 +1419,7 @@ function TaskToolInput({ input }: { input: Record }) { {taskId && (
- Task + {t("taskLabel")} #{taskId} @@ -1419,7 +1429,8 @@ function TaskToolInput({ input }: { input: Record }) { )} {agentName && (
- Name: {agentName} + {t("nameLabel")}{" "} + {agentName}
)}
@@ -1487,6 +1498,7 @@ function TodoWriteToolInput({ input }: { input: Record }) { } function ApplyPatchToolInput({ input }: { input: string }) { + const t = useTranslations("Folder.chat.contentParts") const { files, additions, deletions } = useMemo( () => parseApplyPatchInput(input), [input] @@ -1501,7 +1513,7 @@ function ApplyPatchToolInput({ input }: { input: string }) { return (
- Files: {files.length} + {t("filesCount", { count: files.length })} {additions > 0 && +{additions}} {deletions > 0 && -{deletions}}
@@ -1521,7 +1533,7 @@ function ApplyPatchToolInput({ input }: { input: string }) { ))} {files.length > 8 && (
- +{files.length - 8} more files + {t("moreFiles", { count: files.length - 8 })}
)}
diff --git a/src/components/message/tool-call-block.tsx b/src/components/message/tool-call-block.tsx index d5823c0..9de3f87 100644 --- a/src/components/message/tool-call-block.tsx +++ b/src/components/message/tool-call-block.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import { ChevronRight, ChevronDown, Wrench, AlertCircle } from "lucide-react" +import { useTranslations } from "next-intl" import { cn } from "@/lib/utils" interface ToolCallBlockProps { @@ -17,6 +18,7 @@ export function ToolCallBlock({ content, isError = false, }: ToolCallBlockProps) { + const t = useTranslations("Folder.chat.toolCallBlock") const [expanded, setExpanded] = useState(false) return ( @@ -40,7 +42,7 @@ export function ToolCallBlock({ {type === "tool_use" ? ( <> - {toolName || "Tool"} + {toolName || t("tool")} ) : ( <> @@ -49,7 +51,9 @@ export function ToolCallBlock({ ) : ( )} - {isError ? "Error" : "Result"} + + {isError ? t("error") : t("result")} + )} diff --git a/src/contexts/tab-context.tsx b/src/contexts/tab-context.tsx index e1bab07..6121096 100644 --- a/src/contexts/tab-context.tsx +++ b/src/contexts/tab-context.tsx @@ -10,6 +10,7 @@ import { useMemo, type ReactNode, } from "react" +import { useTranslations } from "next-intl" import { useFolderContext } from "@/contexts/folder-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { saveFolderOpenedConversations } from "@/lib/tauri" @@ -107,6 +108,7 @@ interface TabProviderProps { } export function TabProvider({ children }: TabProviderProps) { + const t = useTranslations("Folder.tabContext") const { activateConversationPane } = useWorkspaceContext() const { folder, @@ -131,7 +133,7 @@ export function TabProvider({ children }: TabProviderProps) { kind: "conversation" as const, conversationId: selectedConversation.id, agentType: selectedConversation.agentType, - title: "Loading...", + title: t("loadingConversation"), isPinned: true, }, ] @@ -188,7 +190,7 @@ export function TabProvider({ children }: TabProviderProps) { kind: "conversation" as const, conversationId: oc.conversation_id, agentType: oc.agent_type, - title: "Loading...", + title: t("loadingConversation"), isPinned: oc.is_pinned, })) @@ -204,7 +206,7 @@ export function TabProvider({ children }: TabProviderProps) { return () => { cancelled = true } - }, [folder, restoredFolderId]) + }, [folder, restoredFolderId, t]) // Sync restored active tab to FolderProvider (deferred to avoid // updating parent during child render) @@ -280,7 +282,7 @@ export function TabProvider({ children }: TabProviderProps) { `${tab.agentType}-${tab.conversationId}` ) if (conv) { - const newTitle = conv.title || "Untitled conversation" + const newTitle = conv.title || t("untitledConversation") const newStatus = conv.status as ConversationStatus | undefined if (tab.title !== newTitle || tab.status !== newStatus) { return { ...tab, title: newTitle, status: newStatus } @@ -289,7 +291,7 @@ export function TabProvider({ children }: TabProviderProps) { } return tab }) - }, [rawTabs, conversationMap]) + }, [rawTabs, conversationMap, t]) const syncFolderContext = useCallback( (tab: TabItem | null) => { @@ -347,7 +349,7 @@ export function TabProvider({ children }: TabProviderProps) { conversationsRef.current.find( (c) => c.id === conversationId && c.agent_type === agentType )?.title ?? - "Untitled conversation" + t("untitledConversation") const tabId = makeConversationTabId(agentType, conversationId) activateTabId = tabId @@ -381,7 +383,7 @@ export function TabProvider({ children }: TabProviderProps) { selectConversation(conversationId, agentType) activateConversationPane() }, - [activateConversationPane, selectConversation] + [activateConversationPane, selectConversation, t] ) const makeReplacementNewConversationTab = useCallback( @@ -389,11 +391,11 @@ export function TabProvider({ children }: TabProviderProps) { id: makeNewConversationTabId(), kind: "new_conversation", agentType: preferred?.agentType ?? "codex", - title: "New Conversation", + title: t("newConversation"), isPinned: true, workingDir: preferred?.workingDir ?? folder?.path, }), - [folder?.path] + [folder?.path, t] ) const closeTab = useCallback( @@ -517,7 +519,7 @@ export function TabProvider({ children }: TabProviderProps) { id: tabId, kind: "new_conversation", agentType, - title: "New Conversation", + title: t("newConversation"), isPinned: true, workingDir, } @@ -527,7 +529,7 @@ export function TabProvider({ children }: TabProviderProps) { startNewConversation(agentType, workingDir) activateConversationPane() }, - [activateConversationPane, startNewConversation, syncFolderContext] + [activateConversationPane, startNewConversation, syncFolderContext, t] ) const linkTabConversation = useCallback( diff --git a/src/hooks/use-connection-lifecycle.ts b/src/hooks/use-connection-lifecycle.ts index 34d557f..e3b1dec 100644 --- a/src/hooks/use-connection-lifecycle.ts +++ b/src/hooks/use-connection-lifecycle.ts @@ -1,6 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" +import { useTranslations } from "next-intl" import { useAcpActions } from "@/contexts/acp-connections-context" import { useTaskContext } from "@/contexts/task-context" import { useConnection, type UseConnectionReturn } from "@/hooks/use-connection" @@ -48,6 +49,7 @@ export function useConnectionLifecycle({ workingDir, sessionId, }: UseConnectionLifecycleOptions): UseConnectionLifecycleReturn { + const t = useTranslations("Folder.chat.connectionLifecycle") const { setActiveKey, touchActivity } = useAcpActions() const { addTask, updateTask, removeTask } = useTaskContext() const conn = useConnection(contextKey) @@ -170,7 +172,11 @@ export function useConnectionLifecycle({ const id = `acp-connect-${Date.now()}` taskIdRef.current = id const agent = AGENT_LABELS[agentType] - addTask(id, `Connecting to ${agent}`, `Establishing connection`) + addTask( + id, + t("tasks.connectingTitle", { agent }), + t("tasks.connectingDescription") + ) } updateTask(taskIdRef.current, { status: "running" }) } else if (status === "connected" || status === "prompting") { @@ -182,7 +188,7 @@ export function useConnectionLifecycle({ if (taskIdRef.current) { updateTask(taskIdRef.current, { status: "failed", - error: "Connection failed", + error: t("errors.connectionFailed"), }) taskIdRef.current = null } @@ -192,7 +198,7 @@ export function useConnectionLifecycle({ taskIdRef.current = null } } - }, [status, addTask, updateTask, removeTask, agentType]) + }, [status, addTask, updateTask, removeTask, agentType, t]) useEffect(() => { if (status === "prompting") return @@ -235,8 +241,8 @@ export function useConnectionLifecycle({ const agent = AGENT_LABELS[agentType] addTask( id, - `Loading ${agent} selectors`, - "Fetching mode and session config options" + t("tasks.loadingSelectorsTitle", { agent }), + t("tasks.loadingSelectorsDescription") ) updateTask(id, { status: "running" }) } @@ -257,6 +263,7 @@ export function useConnectionLifecycle({ addTask, updateTask, clearSelectorTask, + t, ]) // Clean up lingering task on unmount (e.g. tab closed while connecting) diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d1a641a..e343fb8 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -919,6 +919,11 @@ "kindFile": "file" } }, + "tabContext": { + "loadingConversation": "Loading...", + "untitledConversation": "Untitled conversation", + "newConversation": "New Conversation" + }, "fileTreeTab": { "workspace": "Workspace", "retry": "Retry", @@ -1056,6 +1061,21 @@ "diffDescriptionConflict": "{path} · disk vs unsaved" }, "chat": { + "connectionLifecycle": { + "tasks": { + "connectingTitle": "Connecting to {agent}", + "connectingDescription": "Establishing connection", + "loadingSelectorsTitle": "Loading {agent} selectors", + "loadingSelectorsDescription": "Fetching mode and session config options" + }, + "errors": { + "connectionFailed": "Connection failed" + } + }, + "messageThread": { + "emptyTitle": "No messages yet", + "emptyDescription": "Start a conversation to see messages here" + }, "chatInput": { "connecting": "Connecting...", "agentResponding": "Agent is responding...", @@ -1140,6 +1160,8 @@ }, "tool": { "parameters": "Parameters", + "error": "Error", + "result": "Result", "status": { "approvalRequested": "Awaiting Approval", "approvalResponded": "Responded", @@ -1150,9 +1172,36 @@ "outputError": "Error" } }, + "toolCallBlock": { + "tool": "Tool", + "error": "Error", + "result": "Result" + }, "contentParts": { "showingTailOutput": "Showing tail output while streaming for performance.", - "result": "Result" + "result": "Result", + "unknown": "unknown", + "replaceAll": "REPLACE ALL", + "filesCount": "Files: {count}", + "update": "update", + "moreFiles": "+{count} more files", + "timeoutMs": "Timeout: {timeout}ms", + "backgroundTrue": "Background: true", + "offset": "Offset: {offset}", + "limit": "Limit: {limit}", + "pages": "Pages: {pages}", + "mode": "Mode: {mode}", + "cell": "Cell: {cell}", + "pathLabel": "Path:", + "globLabel": "Glob:", + "typeLabel": "Type:", + "outputLabel": "Output:", + "caseInsensitive": "Case insensitive", + "multiline": "Multiline", + "promptLabel": "Prompt", + "subjectLabel": "Subject", + "taskLabel": "Task", + "nameLabel": "Name:" } }, "diffPreview": { diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 640c695..fc1c44b 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -919,6 +919,11 @@ "kindFile": "文件" } }, + "tabContext": { + "loadingConversation": "加载中...", + "untitledConversation": "未命名会话", + "newConversation": "新建会话" + }, "fileTreeTab": { "workspace": "工作区", "retry": "重试", @@ -1056,6 +1061,21 @@ "diffDescriptionConflict": "{path} · 磁盘与未保存内容" }, "chat": { + "connectionLifecycle": { + "tasks": { + "connectingTitle": "正在连接 {agent}", + "connectingDescription": "正在建立连接", + "loadingSelectorsTitle": "正在加载 {agent} 选择项", + "loadingSelectorsDescription": "正在获取模式和会话配置选项" + }, + "errors": { + "connectionFailed": "连接失败" + } + }, + "messageThread": { + "emptyTitle": "暂无消息", + "emptyDescription": "开始一个会话后,消息会显示在这里" + }, "chatInput": { "connecting": "连接中...", "agentResponding": "Agent 正在响应...", @@ -1140,6 +1160,8 @@ }, "tool": { "parameters": "参数", + "error": "错误", + "result": "结果", "status": { "approvalRequested": "等待授权", "approvalResponded": "已响应", @@ -1150,9 +1172,36 @@ "outputError": "错误" } }, + "toolCallBlock": { + "tool": "工具", + "error": "错误", + "result": "结果" + }, "contentParts": { "showingTailOutput": "为保证性能,流式输出时仅显示尾部内容。", - "result": "结果" + "result": "结果", + "unknown": "未知", + "replaceAll": "全部替换", + "filesCount": "文件:{count}", + "update": "更新", + "moreFiles": "+{count} 个更多文件", + "timeoutMs": "超时:{timeout}ms", + "backgroundTrue": "后台:true", + "offset": "偏移:{offset}", + "limit": "限制:{limit}", + "pages": "页码:{pages}", + "mode": "模式:{mode}", + "cell": "单元:{cell}", + "pathLabel": "路径:", + "globLabel": "Glob:", + "typeLabel": "类型:", + "outputLabel": "输出:", + "caseInsensitive": "忽略大小写", + "multiline": "多行", + "promptLabel": "提示词", + "subjectLabel": "主题", + "taskLabel": "任务", + "nameLabel": "名称:" } }, "diffPreview": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 281b78c..f268cab 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -919,6 +919,11 @@ "kindFile": "檔案" } }, + "tabContext": { + "loadingConversation": "載入中...", + "untitledConversation": "未命名會話", + "newConversation": "新增會話" + }, "fileTreeTab": { "workspace": "工作區", "retry": "重試", @@ -1056,6 +1061,21 @@ "diffDescriptionConflict": "{path} · 磁碟與未儲存內容" }, "chat": { + "connectionLifecycle": { + "tasks": { + "connectingTitle": "正在連線 {agent}", + "connectingDescription": "正在建立連線", + "loadingSelectorsTitle": "正在載入 {agent} 選擇項", + "loadingSelectorsDescription": "正在取得模式與會話設定選項" + }, + "errors": { + "connectionFailed": "連線失敗" + } + }, + "messageThread": { + "emptyTitle": "暫無訊息", + "emptyDescription": "開始一個會話後,訊息會顯示在這裡" + }, "chatInput": { "connecting": "連線中...", "agentResponding": "Agent 正在回應...", @@ -1140,6 +1160,8 @@ }, "tool": { "parameters": "參數", + "error": "錯誤", + "result": "結果", "status": { "approvalRequested": "等待授權", "approvalResponded": "已回應", @@ -1150,9 +1172,36 @@ "outputError": "錯誤" } }, + "toolCallBlock": { + "tool": "工具", + "error": "錯誤", + "result": "結果" + }, "contentParts": { "showingTailOutput": "為確保效能,串流輸出時僅顯示尾端內容。", - "result": "結果" + "result": "結果", + "unknown": "未知", + "replaceAll": "全部替換", + "filesCount": "檔案:{count}", + "update": "更新", + "moreFiles": "+{count} 個更多檔案", + "timeoutMs": "逾時:{timeout}ms", + "backgroundTrue": "背景:true", + "offset": "位移:{offset}", + "limit": "限制:{limit}", + "pages": "頁碼:{pages}", + "mode": "模式:{mode}", + "cell": "儲存格:{cell}", + "pathLabel": "路徑:", + "globLabel": "Glob:", + "typeLabel": "類型:", + "outputLabel": "輸出:", + "caseInsensitive": "不區分大小寫", + "multiline": "多行", + "promptLabel": "提示詞", + "subjectLabel": "主題", + "taskLabel": "任務", + "nameLabel": "名稱:" } }, "diffPreview": {