From 835fea6bb135b2d2757445fc647618c828a91f0b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:53:46 -0600 Subject: [PATCH] wip(app): i18n prompt input --- packages/app/src/components/prompt-input.tsx | 102 +++++++++---------- packages/app/src/i18n/en.ts | 46 +++++++++ packages/app/src/i18n/zh.ts | 46 +++++++++ specs/06-app-i18n-audit.md | 57 +++-------- 4 files changed, 158 insertions(+), 93 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 63e9dfbfbd..35a74f43e9 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (props) => { 0} - fallback={
No matching results
} + fallback={
{language.t("prompt.popover.emptyResults")}
} > {(item) => ( @@ -1386,7 +1386,7 @@ export const PromptInput: Component = (props) => { 0} - fallback={
No matching commands
} + fallback={
{language.t("prompt.popover.emptyCommands")}
} > {(cmd) => ( @@ -1408,7 +1408,7 @@ export const PromptInput: Component = (props) => {
- custom + {language.t("prompt.slash.badge.custom")} @@ -1437,7 +1437,7 @@ export const PromptInput: Component = (props) => {
- Drop images or PDFs here + {language.t("prompt.dropzone.label")}
@@ -1450,7 +1450,7 @@ export const PromptInput: Component = (props) => {
{getDirectory(path())} {getFilename(path())} - active + {language.t("prompt.context.active")}
= (props) => { onClick={() => prompt.context.addActive()} > - Include active file + {language.t("prompt.context.includeActiveFile")} @@ -1563,7 +1563,7 @@ export const PromptInput: Component = (props) => {
{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]) })}
@@ -1681,7 +1681,7 @@ export const PromptInput: Component = (props) => {
- + @@ -1695,13 +1695,13 @@ export const PromptInput: Component = (props) => {
- Stop + {language.t("prompt.action.stop")} ESC
- Send + {language.t("prompt.action.send")}
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index cd900e1c0e..9e100d5200 100644 --- a/packages/app/src/i18n/en.ts +++ b/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", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index e8b37fd63b..28a39612a5 100644 --- a/packages/app/src/i18n/zh.ts +++ b/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", diff --git a/specs/06-app-i18n-audit.md b/specs/06-app-i18n-audit.md index e8ae0b24e5..de77e74416 100644 --- a/specs/06-app-i18n-audit.md +++ b/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). +Completed (2026-01-20): -**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" - -**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`