summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-20 07:53:46 -0600
committerAdam <[email protected]>2026-01-20 17:58:06 -0600
commit835fea6bb135b2d2757445fc647618c828a91f0b (patch)
tree5ae60b8f4a4453a8a4ae64dd7c858c05040b000a
parent7138bd021c4aa73204b2c74e3c954b38d20acd75 (diff)
downloadopencode-835fea6bb135b2d2757445fc647618c828a91f0b.tar.gz
opencode-835fea6bb135b2d2757445fc647618c828a91f0b.zip
wip(app): i18n prompt input
-rw-r--r--packages/app/src/components/prompt-input.tsx102
-rw-r--r--packages/app/src/i18n/en.ts46
-rw-r--r--packages/app/src/i18n/zh.ts46
-rw-r--r--specs/06-app-i18n-audit.md57
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 63e9dfbfb..35a74f43e 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<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>
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index cd900e1c0..9e100d520 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 e8b37fd63..28a39612a 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 e8ae0b24e..de77e7441 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).
-
-**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`