Adam 2 месяцев назад
Родитель
Сommit
835fea6bb1

+ 51 - 51
packages/app/src/components/prompt-input.tsx

@@ -67,33 +67,33 @@ interface PromptInputProps {
   onNewSessionWorktreeReset?: () => void
 }
 
-const PLACEHOLDERS = [
-  "Fix a TODO in the codebase",
-  "What is the tech stack of this project?",
-  "Fix broken tests",
-  "Explain how authentication works",
-  "Find and fix security vulnerabilities",
-  "Add unit tests for the user service",
-  "Refactor this function to be more readable",
-  "What does this error mean?",
-  "Help me debug this issue",
-  "Generate API documentation",
-  "Optimize database queries",
-  "Add input validation",
-  "Create a new component for...",
-  "How do I deploy this project?",
-  "Review my code for best practices",
-  "Add error handling to this function",
-  "Explain this regex pattern",
-  "Convert this to TypeScript",
-  "Add logging throughout the codebase",
-  "What dependencies are outdated?",
-  "Help me write a migration script",
-  "Implement caching for this endpoint",
-  "Add pagination to this list",
-  "Create a CLI command for...",
-  "How do environment variables work here?",
-]
+const EXAMPLES = [
+  "prompt.example.1",
+  "prompt.example.2",
+  "prompt.example.3",
+  "prompt.example.4",
+  "prompt.example.5",
+  "prompt.example.6",
+  "prompt.example.7",
+  "prompt.example.8",
+  "prompt.example.9",
+  "prompt.example.10",
+  "prompt.example.11",
+  "prompt.example.12",
+  "prompt.example.13",
+  "prompt.example.14",
+  "prompt.example.15",
+  "prompt.example.16",
+  "prompt.example.17",
+  "prompt.example.18",
+  "prompt.example.19",
+  "prompt.example.20",
+  "prompt.example.21",
+  "prompt.example.22",
+  "prompt.example.23",
+  "prompt.example.24",
+  "prompt.example.25",
+] as const
 
 interface SlashCommand {
   id: string
@@ -186,7 +186,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     popover: null,
     historyIndex: -1,
     savedPrompt: null,
-    placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
+    placeholder: Math.floor(Math.random() * EXAMPLES.length),
     dragging: false,
     mode: "normal",
     applyingHistory: false,
@@ -259,7 +259,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     params.id
     if (params.id) return
     const interval = setInterval(() => {
-      setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
+      setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
     }, 6500)
     onCleanup(() => clearInterval(interval))
   })
@@ -314,8 +314,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
     if (fileItems.length > 0) {
       showToast({
-        title: "Unsupported paste",
-        description: "Only images or PDFs can be pasted here.",
+        title: language.t("prompt.toast.pasteUnsupported.title"),
+        description: language.t("prompt.toast.pasteUnsupported.description"),
       })
       return
     }
@@ -999,8 +999,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     const currentAgent = local.agent.current()
     if (!currentModel || !currentAgent) {
       showToast({
-        title: "Select an agent and model",
-        description: "Choose an agent and model before sending a prompt.",
+        title: language.t("prompt.toast.modelAgentRequired.title"),
+        description: language.t("prompt.toast.modelAgentRequired.description"),
       })
       return
     }
@@ -1011,7 +1011,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         if (data?.message) return data.message
       }
       if (err instanceof Error) return err.message
-      return "Request failed"
+      return language.t("common.requestFailed")
     }
 
     addToHistory(currentPrompt, mode)
@@ -1032,7 +1032,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           .then((x) => x.data)
           .catch((err) => {
             showToast({
-              title: "Failed to create worktree",
+              title: language.t("prompt.toast.worktreeCreateFailed.title"),
               description: errorMessage(err),
             })
             return undefined
@@ -1040,8 +1040,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
         if (!createdWorktree?.directory) {
           showToast({
-            title: "Failed to create worktree",
-            description: "Request failed",
+            title: language.t("prompt.toast.worktreeCreateFailed.title"),
+            description: language.t("common.requestFailed"),
           })
           return
         }
@@ -1072,7 +1072,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         .then((x) => x.data ?? undefined)
         .catch((err) => {
           showToast({
-            title: "Failed to create session",
+            title: language.t("prompt.toast.sessionCreateFailed.title"),
             description: errorMessage(err),
           })
           return undefined
@@ -1116,7 +1116,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         })
         .catch((err) => {
           showToast({
-            title: "Failed to send shell command",
+            title: language.t("prompt.toast.shellSendFailed.title"),
             description: errorMessage(err),
           })
           restoreInput()
@@ -1148,7 +1148,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           })
           .catch((err) => {
             showToast({
-              title: "Failed to send command",
+              title: language.t("prompt.toast.commandSendFailed.title"),
               description: errorMessage(err),
             })
             restoreInput()
@@ -1316,7 +1316,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       })
       .catch((err) => {
         showToast({
-          title: "Failed to send prompt",
+          title: language.t("prompt.toast.promptSendFailed.title"),
           description: errorMessage(err),
         })
         removeOptimisticMessage()
@@ -1340,7 +1340,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <Match when={store.popover === "at"}>
               <Show
                 when={atFlat().length > 0}
-                fallback={<div class="text-text-weak px-2 py-1">No matching results</div>}
+                fallback={<div class="text-text-weak px-2 py-1">{language.t("prompt.popover.emptyResults")}</div>}
               >
                 <For each={atFlat().slice(0, 10)}>
                   {(item) => (
@@ -1386,7 +1386,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <Match when={store.popover === "slash"}>
               <Show
                 when={slashFlat().length > 0}
-                fallback={<div class="text-text-weak px-2 py-1">No matching commands</div>}
+                fallback={<div class="text-text-weak px-2 py-1">{language.t("prompt.popover.emptyCommands")}</div>}
               >
                 <For each={slashFlat()}>
                   {(cmd) => (
@@ -1408,7 +1408,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                       <div class="flex items-center gap-2 shrink-0">
                         <Show when={cmd.type === "custom"}>
                           <span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
-                            custom
+                            {language.t("prompt.slash.badge.custom")}
                           </span>
                         </Show>
                         <Show when={command.keybind(cmd.id)}>
@@ -1437,7 +1437,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           <div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
             <div class="flex flex-col items-center gap-2 text-text-weak">
               <Icon name="photo" class="size-8" />
-              <span class="text-14-regular">Drop images or PDFs here</span>
+              <span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
             </div>
           </div>
         </Show>
@@ -1450,7 +1450,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   <div class="flex items-center text-12-regular min-w-0">
                     <span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
                     <span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
-                    <span class="text-text-weak whitespace-nowrap ml-1">active</span>
+                    <span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
                   </div>
                   <IconButton
                     type="button"
@@ -1469,7 +1469,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 onClick={() => prompt.context.addActive()}
               >
                 <Icon name="plus-small" size="small" />
-                <span>Include active file</span>
+                <span>{language.t("prompt.context.includeActiveFile")}</span>
               </button>
             </Show>
             <For each={prompt.context.items()}>
@@ -1563,7 +1563,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
               {store.mode === "shell"
                 ? language.t("prompt.placeholder.shell")
-                : language.t("prompt.placeholder.normal", { example: PLACEHOLDERS[store.placeholder] })}
+                : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
             </div>
           </Show>
         </div>
@@ -1681,7 +1681,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <div class="flex items-center gap-2">
               <SessionContextUsage />
               <Show when={store.mode === "normal"}>
-                <Tooltip placement="top" value="Attach file">
+                <Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
                   <Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
                     <Icon name="photo" class="size-4.5" />
                   </Button>
@@ -1695,13 +1695,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 <Switch>
                   <Match when={working()}>
                     <div class="flex items-center gap-2">
-                      <span>Stop</span>
+                      <span>{language.t("prompt.action.stop")}</span>
                       <span class="text-icon-base text-12-medium text-[10px]!">ESC</span>
                     </div>
                   </Match>
                   <Match when={true}>
                     <div class="flex items-center gap-2">
-                      <span>Send</span>
+                      <span>{language.t("prompt.action.send")}</span>
                       <Icon name="enter" size="small" class="text-icon-base" />
                     </div>
                   </Match>

+ 46 - 0
packages/app/src/i18n/en.ts

@@ -110,6 +110,52 @@ export const dict = {
   "prompt.mode.shell": "Shell",
   "prompt.mode.shell.exit": "esc to exit",
 
+  "prompt.example.1": "Fix a TODO in the codebase",
+  "prompt.example.2": "What is the tech stack of this project?",
+  "prompt.example.3": "Fix broken tests",
+  "prompt.example.4": "Explain how authentication works",
+  "prompt.example.5": "Find and fix security vulnerabilities",
+  "prompt.example.6": "Add unit tests for the user service",
+  "prompt.example.7": "Refactor this function to be more readable",
+  "prompt.example.8": "What does this error mean?",
+  "prompt.example.9": "Help me debug this issue",
+  "prompt.example.10": "Generate API documentation",
+  "prompt.example.11": "Optimize database queries",
+  "prompt.example.12": "Add input validation",
+  "prompt.example.13": "Create a new component for...",
+  "prompt.example.14": "How do I deploy this project?",
+  "prompt.example.15": "Review my code for best practices",
+  "prompt.example.16": "Add error handling to this function",
+  "prompt.example.17": "Explain this regex pattern",
+  "prompt.example.18": "Convert this to TypeScript",
+  "prompt.example.19": "Add logging throughout the codebase",
+  "prompt.example.20": "What dependencies are outdated?",
+  "prompt.example.21": "Help me write a migration script",
+  "prompt.example.22": "Implement caching for this endpoint",
+  "prompt.example.23": "Add pagination to this list",
+  "prompt.example.24": "Create a CLI command for...",
+  "prompt.example.25": "How do environment variables work here?",
+
+  "prompt.popover.emptyResults": "No matching results",
+  "prompt.popover.emptyCommands": "No matching commands",
+  "prompt.dropzone.label": "Drop images or PDFs here",
+  "prompt.slash.badge.custom": "custom",
+  "prompt.context.active": "active",
+  "prompt.context.includeActiveFile": "Include active file",
+  "prompt.action.attachFile": "Attach file",
+  "prompt.action.send": "Send",
+  "prompt.action.stop": "Stop",
+
+  "prompt.toast.pasteUnsupported.title": "Unsupported paste",
+  "prompt.toast.pasteUnsupported.description": "Only images or PDFs can be pasted here.",
+  "prompt.toast.modelAgentRequired.title": "Select an agent and model",
+  "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
+  "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
+  "prompt.toast.sessionCreateFailed.title": "Failed to create session",
+  "prompt.toast.shellSendFailed.title": "Failed to send shell command",
+  "prompt.toast.commandSendFailed.title": "Failed to send command",
+  "prompt.toast.promptSendFailed.title": "Failed to send prompt",
+
   "dialog.mcp.title": "MCPs",
   "dialog.mcp.description": "{{enabled}} of {{total}} enabled",
   "dialog.mcp.empty": "No MCPs configured",

+ 46 - 0
packages/app/src/i18n/zh.ts

@@ -114,6 +114,52 @@ export const dict = {
   "prompt.mode.shell": "Shell",
   "prompt.mode.shell.exit": "按 esc 退出",
 
+  "prompt.example.1": "修复代码库中的一个 TODO",
+  "prompt.example.2": "这个项目的技术栈是什么?",
+  "prompt.example.3": "修复失败的测试",
+  "prompt.example.4": "解释认证是如何工作的",
+  "prompt.example.5": "查找并修复安全漏洞",
+  "prompt.example.6": "为用户服务添加单元测试",
+  "prompt.example.7": "重构这个函数,让它更易读",
+  "prompt.example.8": "这个错误是什么意思?",
+  "prompt.example.9": "帮我调试这个问题",
+  "prompt.example.10": "生成 API 文档",
+  "prompt.example.11": "优化数据库查询",
+  "prompt.example.12": "添加输入校验",
+  "prompt.example.13": "创建一个新的组件用于...",
+  "prompt.example.14": "我该如何部署这个项目?",
+  "prompt.example.15": "审查我的代码并给出最佳实践建议",
+  "prompt.example.16": "为这个函数添加错误处理",
+  "prompt.example.17": "解释这个正则表达式",
+  "prompt.example.18": "把它转换成 TypeScript",
+  "prompt.example.19": "在整个代码库中添加日志",
+  "prompt.example.20": "哪些依赖已经过期?",
+  "prompt.example.21": "帮我写一个迁移脚本",
+  "prompt.example.22": "为这个接口实现缓存",
+  "prompt.example.23": "给这个列表添加分页",
+  "prompt.example.24": "创建一个 CLI 命令用于...",
+  "prompt.example.25": "这里的环境变量是怎么工作的?",
+
+  "prompt.popover.emptyResults": "没有匹配的结果",
+  "prompt.popover.emptyCommands": "没有匹配的命令",
+  "prompt.dropzone.label": "将图片或 PDF 拖到这里",
+  "prompt.slash.badge.custom": "自定义",
+  "prompt.context.active": "当前",
+  "prompt.context.includeActiveFile": "包含当前文件",
+  "prompt.action.attachFile": "附加文件",
+  "prompt.action.send": "发送",
+  "prompt.action.stop": "停止",
+
+  "prompt.toast.pasteUnsupported.title": "不支持的粘贴",
+  "prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。",
+  "prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
+  "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
+  "prompt.toast.worktreeCreateFailed.title": "创建工作树失败",
+  "prompt.toast.sessionCreateFailed.title": "创建会话失败",
+  "prompt.toast.shellSendFailed.title": "发送 shell 命令失败",
+  "prompt.toast.commandSendFailed.title": "发送命令失败",
+  "prompt.toast.promptSendFailed.title": "发送提示失败",
+
   "dialog.mcp.title": "MCPs",
   "dialog.mcp.description": "已启用 {{enabled}} / {{total}}",
   "dialog.mcp.empty": "未配置 MCPs",

+ 15 - 42
specs/06-app-i18n-audit.md

@@ -9,8 +9,8 @@ This report documents the remaining user-facing strings in `packages/app/src` th
 ## Current State
 
 - The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
-- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx` (plus new keys added in both dictionaries).
-- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (242 keys each; no missing or extra keys).
+- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx` (plus new keys added in both dictionaries).
+- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (285 keys each; no missing or extra keys).
 
 ## Methodology
 
@@ -59,38 +59,13 @@ This is the largest remaining untranslated surface and is user-visible during ap
 
 File: `packages/app/src/components/prompt-input.tsx`
 
-This is the largest remaining i18n surface (placeholders, empty states, tooltips, toasts).
-
-**Untranslated prompt examples**
-- `PLACEHOLDERS` array is English-only (e.g. "Fix broken tests", "Explain how authentication works", ...)
-- Note: the placeholder key `prompt.placeholder.normal` exists and interpolates `{{example}}`, but the examples are not localized.
-
-**Toast copy**
-- "Unsupported paste" / "Only images or PDFs can be pasted here."
-- "Select an agent and model" / "Choose an agent and model before sending a prompt."
-- Failure toasts:
-  - "Failed to create worktree"
-  - "Failed to create session"
-  - "Failed to send shell command"
-  - "Failed to send command"
-  - "Failed to send prompt"
-- Fallback return string: "Request failed" (you already have `common.requestFailed`)
-
-**Empty states / popovers / overlays**
-- "No matching results"
-- "No matching commands"
-- Drag/drop overlay: "Drop images or PDFs here"
-
-**Labels / badges / buttons**
-- Slash badge label: "custom"
-- File pill label: "active"
-- Action: "Include active file"
-- Send/Stop labels: "Send", "Stop" (and the "ESC" hint)
-- Tooltip: "Attach file"
+Completed (2026-01-20):
 
-**Recommendation:**
-- Introduce a `prompt.*` namespace for UI strings and toast titles/descriptions.
-- Handle prompt examples as locale-specific arrays OR enumerated keys (e.g. `prompt.example.1`, `prompt.example.2`, ...).
+- Localized placeholder examples by replacing the hardcoded `PLACEHOLDERS` list with `prompt.example.*` keys.
+- Localized toast titles/descriptions via `prompt.toast.*` and reused `common.requestFailed` for fallback error text.
+- Localized popover empty states and drag/drop overlay copy (`prompt.popover.*`, `prompt.dropzone.label`).
+- Localized smaller labels (slash "custom" badge, attach button tooltip, Send/Stop tooltip labels).
+- Kept the `ESC` keycap itself untranslated (key label).
 
 ### 3) Provider Connection / Auth Flow
 
@@ -275,13 +250,12 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
 
 ## Prioritized Implementation Plan
 
-1. `packages/app/src/components/prompt-input.tsx`
-2. `packages/app/src/components/dialog-connect-provider.tsx`
-3. `packages/app/src/components/session/session-header.tsx`
-4. `packages/app/src/pages/error.tsx`
-5. `packages/app/src/components/session/session-new-view.tsx`
-6. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`)
-7. Small stragglers:
+1. `packages/app/src/components/dialog-connect-provider.tsx`
+2. `packages/app/src/components/session/session-header.tsx`
+3. `packages/app/src/pages/error.tsx`
+4. `packages/app/src/components/session/session-new-view.tsx`
+5. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`)
+6. Small stragglers:
    - `packages/app/src/components/session-lsp-indicator.tsx`
    - `packages/app/src/components/session/session-sortable-tab.tsx`
    - `packages/app/src/components/titlebar.tsx`
@@ -290,7 +264,7 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
    - `packages/app/src/context/global-sync.tsx`
    - `packages/app/src/context/file.tsx` + `packages/app/src/context/local.tsx`
    - `packages/app/src/utils/prompt.ts`
-8. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
+7. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
 
 ## Suggested Key Naming Conventions
 
@@ -313,7 +287,6 @@ Pages:
 - `packages/app/src/pages/error.tsx`
 
 Components:
-- `packages/app/src/components/prompt-input.tsx`
 - `packages/app/src/components/dialog-connect-provider.tsx`
 - `packages/app/src/components/session/session-header.tsx`
 - `packages/app/src/components/session/session-new-view.tsx`