diff options
| author | Adam <[email protected]> | 2026-01-20 09:38:18 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-20 17:58:06 -0600 |
| commit | 7a359ff67c7e5cd86b638d8512e023ad2cf16b86 (patch) | |
| tree | e1229dc142f085466b1637b47bd1fbaf3c65c2d0 | |
| parent | 835fea6bb135b2d2757445fc647618c828a91f0b (diff) | |
| download | opencode-7a359ff67c7e5cd86b638d8512e023ad2cf16b86.tar.gz opencode-7a359ff67c7e5cd86b638d8512e023ad2cf16b86.zip | |
wip(app): i18n
| -rw-r--r-- | packages/app/src/components/dialog-connect-provider.tsx | 82 | ||||
| -rw-r--r-- | packages/app/src/i18n/en.ts | 35 | ||||
| -rw-r--r-- | packages/app/src/i18n/zh.ts | 30 | ||||
| -rw-r--r-- | specs/06-app-i18n-audit.md | 39 |
4 files changed, 124 insertions, 62 deletions
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 789a5d3b7..fa72ccef3 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -14,6 +14,7 @@ import { iife } from "@opencode-ai/util/iife" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" +import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" @@ -25,6 +26,7 @@ export function DialogConnectProvider(props: { provider: string }) { const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const platform = usePlatform() + const language = useLanguage() const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => @@ -44,6 +46,12 @@ export function DialogConnectProvider(props: { provider: string }) { const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) + const methodLabel = (value?: { type?: string; label?: string }) => { + if (!value) return "" + if (value.type === "api") return language.t("provider.connect.method.apiKey") + return value.label ?? "" + } + async function selectMethod(index: number) { const method = methods()[index] setStore( @@ -112,8 +120,8 @@ export function DialogConnectProvider(props: { provider: string }) { showToast({ variant: "success", icon: "circle-check", - title: `${provider().name} connected`, - description: `${provider().name} models are now available to use.`, + title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), + description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), }) } @@ -142,16 +150,18 @@ export function DialogConnectProvider(props: { provider: string }) { <div class="text-16-medium text-text-strong"> <Switch> <Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}> - Login with Claude Pro/Max + {language.t("provider.connect.title.anthropicProMax")} </Match> - <Match when={true}>Connect {provider().name}</Match> + <Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match> </Switch> </div> </div> <div class="px-2.5 pb-10 flex flex-col gap-6"> <Switch> <Match when={store.methodIndex === undefined}> - <div class="text-14-regular text-text-base">Select login method for {provider().name}.</div> + <div class="text-14-regular text-text-base"> + {language.t("provider.connect.selectMethod", { provider: provider().name })} + </div> <div class=""> <List ref={(ref) => { @@ -169,7 +179,7 @@ export function DialogConnectProvider(props: { provider: string }) { <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center"> <div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" /> </div> - <span>{i.label}</span> + <span>{methodLabel(i)}</span> </div> )} </List> @@ -179,7 +189,7 @@ export function DialogConnectProvider(props: { provider: string }) { <div class="text-14-regular text-text-base"> <div class="flex items-center gap-x-2"> <Spinner /> - <span>Authorization in progress...</span> + <span>{language.t("provider.connect.status.inProgress")}</span> </div> </div> </Match> @@ -187,7 +197,7 @@ export function DialogConnectProvider(props: { provider: string }) { <div class="text-14-regular text-text-base"> <div class="flex items-center gap-x-2"> <Icon name="circle-ban-sign" class="text-icon-critical-base" /> - <span>Authorization failed: {store.error}</span> + <span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span> </div> </div> </Match> @@ -206,7 +216,7 @@ export function DialogConnectProvider(props: { provider: string }) { const apiKey = formData.get("apiKey") as string if (!apiKey?.trim()) { - setFormStore("error", "API key is required") + setFormStore("error", language.t("provider.connect.apiKey.required")) return } @@ -226,26 +236,20 @@ export function DialogConnectProvider(props: { provider: string }) { <Switch> <Match when={provider().id === "opencode"}> <div class="flex flex-col gap-4"> + <div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line1")}</div> + <div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div> <div class="text-14-regular text-text-base"> - OpenCode Zen gives you access to a curated set of reliable optimized models for coding - agents. - </div> - <div class="text-14-regular text-text-base"> - With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more. - </div> - <div class="text-14-regular text-text-base"> - Visit{" "} + {language.t("provider.connect.opencodeZen.visit.prefix")} <Link href="https://opencode.ai/zen" tabIndex={-1}> opencode.ai/zen - </Link>{" "} - to collect your API key. + </Link> + {language.t("provider.connect.opencodeZen.visit.suffix")} </div> </div> </Match> <Match when={true}> <div class="text-14-regular text-text-base"> - Enter your {provider().name} API key to connect your account and use {provider().name} models - in OpenCode. + {language.t("provider.connect.apiKey.description", { provider: provider().name })} </div> </Match> </Switch> @@ -253,8 +257,8 @@ export function DialogConnectProvider(props: { provider: string }) { <TextField autofocus type="text" - label={`${provider().name} API key`} - placeholder="API key" + label={language.t("provider.connect.apiKey.label", { provider: provider().name })} + placeholder={language.t("provider.connect.apiKey.placeholder")} name="apiKey" value={formStore.value} onChange={setFormStore.bind(null, "value")} @@ -262,7 +266,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} /> <Button class="w-auto" type="submit" size="large" variant="primary"> - Submit + {language.t("common.submit")} </Button> </form> </div> @@ -292,7 +296,7 @@ export function DialogConnectProvider(props: { provider: string }) { const code = formData.get("code") as string if (!code?.trim()) { - setFormStore("error", "Authorization code is required") + setFormStore("error", language.t("provider.connect.oauth.code.required")) return } @@ -306,21 +310,22 @@ export function DialogConnectProvider(props: { provider: string }) { await complete() return } - setFormStore("error", "Invalid authorization code") + setFormStore("error", language.t("provider.connect.oauth.code.invalid")) } return ( <div class="flex flex-col gap-6"> <div class="text-14-regular text-text-base"> - Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization - code to connect your account and use {provider().name} models in OpenCode. + {language.t("provider.connect.oauth.code.visit.prefix")} + <Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link> + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} </div> <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4"> <TextField autofocus type="text" - label={`${method()?.label} authorization code`} - placeholder="Authorization code" + label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })} + placeholder={language.t("provider.connect.oauth.code.placeholder")} name="code" value={formStore.value} onChange={setFormStore.bind(null, "value")} @@ -328,7 +333,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} /> <Button class="w-auto" type="submit" size="large" variant="primary"> - Submit + {language.t("common.submit")} </Button> </form> </div> @@ -361,13 +366,20 @@ export function DialogConnectProvider(props: { provider: string }) { return ( <div class="flex flex-col gap-6"> <div class="text-14-regular text-text-base"> - Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to - connect your account and use {provider().name} models in OpenCode. + {language.t("provider.connect.oauth.auto.visit.prefix")} + <Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link> + {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })} </div> - <TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable /> + <TextField + label={language.t("provider.connect.oauth.auto.confirmationCode")} + class="font-mono" + value={code()} + readOnly + copyable + /> <div class="text-14-regular text-text-base flex items-center gap-4"> <Spinner /> - <span>Waiting for authorization...</span> + <span>{language.t("provider.connect.status.waiting")}</span> </div> </div> ) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 9e100d520..67a08c829 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -95,12 +95,47 @@ export const dict = { "dialog.provider.viewAll": "View all providers", + "provider.connect.title": "Connect {{provider}}", + "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", + "provider.connect.selectMethod": "Select login method for {{provider}}.", + "provider.connect.method.apiKey": "API key", + "provider.connect.status.inProgress": "Authorization in progress...", + "provider.connect.status.waiting": "Waiting for authorization...", + "provider.connect.status.failed": "Authorization failed: {{error}}", + "provider.connect.apiKey.description": + "Enter your {{provider}} API key to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API key", + "provider.connect.apiKey.placeholder": "API key", + "provider.connect.apiKey.required": "API key is required", + "provider.connect.opencodeZen.line1": + "OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.", + "provider.connect.opencodeZen.line2": + "With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.", + "provider.connect.opencodeZen.visit.prefix": "Visit ", + "provider.connect.opencodeZen.visit.suffix": " to collect your API key.", + "provider.connect.oauth.code.visit.prefix": "Visit ", + "provider.connect.oauth.code.visit.link": "this link", + "provider.connect.oauth.code.visit.suffix": + " to collect your authorization code to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.code.label": "{{method}} authorization code", + "provider.connect.oauth.code.placeholder": "Authorization code", + "provider.connect.oauth.code.required": "Authorization code is required", + "provider.connect.oauth.code.invalid": "Invalid authorization code", + "provider.connect.oauth.auto.visit.prefix": "Visit ", + "provider.connect.oauth.auto.visit.link": "this link", + "provider.connect.oauth.auto.visit.suffix": + " and enter the code below to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Confirmation code", + "provider.connect.toast.connected.title": "{{provider}} connected", + "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + "model.tag.free": "Free", "model.tag.latest": "Latest", "common.search.placeholder": "Search", "common.loading": "Loading", "common.cancel": "Cancel", + "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", "common.default": "Default", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 28a39612a..ef2be2f37 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -99,12 +99,42 @@ export const dict = { "dialog.provider.viewAll": "查看全部提供商", + "provider.connect.title": "连接 {{provider}}", + "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", + "provider.connect.selectMethod": "选择 {{provider}} 的登录方式。", + "provider.connect.method.apiKey": "API 密钥", + "provider.connect.status.inProgress": "正在授权...", + "provider.connect.status.waiting": "等待授权...", + "provider.connect.status.failed": "授权失败: {{error}}", + "provider.connect.apiKey.description": "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.apiKey.label": "{{provider}} API 密钥", + "provider.connect.apiKey.placeholder": "API 密钥", + "provider.connect.apiKey.required": "API 密钥为必填项", + "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", + "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", + "provider.connect.opencodeZen.visit.prefix": "访问 ", + "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", + "provider.connect.oauth.code.visit.prefix": "访问 ", + "provider.connect.oauth.code.visit.link": "此链接", + "provider.connect.oauth.code.visit.suffix": " 获取授权码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.code.label": "{{method}} 授权码", + "provider.connect.oauth.code.placeholder": "授权码", + "provider.connect.oauth.code.required": "授权码为必填项", + "provider.connect.oauth.code.invalid": "授权码无效", + "provider.connect.oauth.auto.visit.prefix": "访问 ", + "provider.connect.oauth.auto.visit.link": "此链接", + "provider.connect.oauth.auto.visit.suffix": " 并输入以下代码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.auto.confirmationCode": "确认码", + "provider.connect.toast.connected.title": "{{provider}} 已连接", + "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", + "model.tag.free": "免费", "model.tag.latest": "最新", "common.search.placeholder": "搜索", "common.loading": "加载中", "common.cancel": "取消", + "common.submit": "提交", "common.save": "保存", "common.saving": "保存中...", "common.default": "默认", diff --git a/specs/06-app-i18n-audit.md b/specs/06-app-i18n-audit.md index de77e7441..e47dac30d 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`, `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). +- 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`, `packages/app/src/components/dialog-connect-provider.tsx` (plus new keys added in both dictionaries). +- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (314 keys each; no missing or extra keys). ## Methodology @@ -71,24 +71,11 @@ Completed (2026-01-20): File: `packages/app/src/components/dialog-connect-provider.tsx` -This flow is copy-heavy and user-facing. - -**Representative untranslated strings** -- Login method label: "API key" -- Status text: "Authorization in progress...", "Waiting for authorization..." -- Validation: "API key is required", "Authorization code is required", "Invalid authorization code" -- Field labels/placeholders: "Confirmation code", placeholder "API key", placeholder "Authorization code" -- Instructional text: - - "Visit this link ..." - - Provider-specific guidance and OpenCode Zen onboarding paragraphs -- Buttons: "Submit" -- Success toast: - - "{provider} connected" - - "{provider} models are now available to use." +Completed (2026-01-20): -**Recommendation:** -- Add a `provider.connect.*` namespace. -- Consider adding shared common keys like `common.submit` if it is used elsewhere. +- Localized all user-visible copy via `provider.connect.*` keys (titles, statuses, validations, instructions, OpenCode Zen onboarding). +- Added `common.submit` and used it for both API + OAuth submit buttons. +- Localized the success toast via `provider.connect.toast.connected.*`. ### 4) Session Header (Share/Publish UI) @@ -250,12 +237,11 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr ## Prioritized Implementation Plan -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: +1. `packages/app/src/components/session/session-header.tsx` +2. `packages/app/src/pages/error.tsx` +3. `packages/app/src/components/session/session-new-view.tsx` +4. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`) +5. 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` @@ -264,7 +250,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` -7. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`). +6. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`). ## Suggested Key Naming Conventions @@ -287,7 +273,6 @@ Pages: - `packages/app/src/pages/error.tsx` Components: -- `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` - `packages/app/src/components/session-context-usage.tsx` |
