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": {