diff options
| author | Adam <[email protected]> | 2026-01-20 05:40:44 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-20 17:58:06 -0600 |
| commit | 92beae14100af23c20afe7b6bc2bb393643d698f (patch) | |
| tree | 1047cf04130c98edac5dd322613608e3ec4b5f46 /packages/app | |
| parent | 0470717c7fbb9ff175b70c6d76ffb2330ef40a1a (diff) | |
| download | opencode-92beae14100af23c20afe7b6bc2bb393643d698f.tar.gz opencode-92beae14100af23c20afe7b6bc2bb393643d698f.zip | |
wip(app): i18n
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/components/dialog-edit-project.tsx | 23 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-fork.tsx | 8 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-manage-models.tsx | 8 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-directory.tsx | 9 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-file.tsx | 18 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-mcp.tsx | 19 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-model-unpaid.tsx | 22 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-model.tsx | 20 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-provider.tsx | 26 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-server.tsx | 26 | ||||
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 38 | ||||
| -rw-r--r-- | packages/app/src/components/session/session-context-tab.tsx | 55 | ||||
| -rw-r--r-- | packages/app/src/context/command.tsx | 4 | ||||
| -rw-r--r-- | packages/app/src/i18n/en.ts | 196 | ||||
| -rw-r--r-- | packages/app/src/i18n/zh.ts | 210 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 59 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 171 |
17 files changed, 687 insertions, 225 deletions
diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 2e414a437..b6e2f822e 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -9,12 +9,14 @@ import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject, getAvatarColors } from "@/context/layout" import { getFilename } from "@opencode-ai/util/path" import { Avatar } from "@opencode-ai/ui/avatar" +import { useLanguage } from "@/context/language" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export function DialogEditProject(props: { project: LocalProject }) { const dialog = useDialog() const globalSDK = useGlobalSDK() + const language = useLanguage() const folderName = createMemo(() => getFilename(props.project.worktree)) const defaultName = createMemo(() => props.project.name || folderName()) @@ -81,20 +83,20 @@ export function DialogEditProject(props: { project: LocalProject }) { } return ( - <Dialog title="Edit project" class="w-full max-w-[480px] mx-auto"> + <Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto"> <form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0"> <div class="flex flex-col gap-4"> <TextField autofocus type="text" - label="Name" + label={language.t("dialog.project.edit.name")} placeholder={folderName()} value={store.name} onChange={(v) => setStore("name", v)} /> <div class="flex flex-col gap-2"> - <label class="text-12-medium text-text-weak">Icon</label> + <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label> <div class="flex gap-3 items-start"> <div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}> <div @@ -128,7 +130,11 @@ export function DialogEditProject(props: { project: LocalProject }) { </div> } > - <img src={store.iconUrl} alt="Project icon" class="size-full object-cover" /> + <img + src={store.iconUrl} + alt={language.t("dialog.project.edit.icon.alt")} + class="size-full object-cover" + /> </Show> </div> <div @@ -172,14 +178,15 @@ export function DialogEditProject(props: { project: LocalProject }) { </div> <input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} /> <div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center"> - <span>Recommended size 128x128px</span> + <span>{language.t("dialog.project.edit.icon.hint")}</span> + <span>{language.t("dialog.project.edit.icon.recommended")}</span> </div> </div> </div> <Show when={!store.iconUrl}> <div class="flex flex-col gap-2"> - <label class="text-12-medium text-text-weak">Color</label> + <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label> <div class="flex gap-1.5"> <For each={AVATAR_COLOR_KEYS}> {(color) => ( @@ -209,10 +216,10 @@ export function DialogEditProject(props: { project: LocalProject }) { <div class="flex justify-end gap-2"> <Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}> - Cancel + {language.t("common.cancel")} </Button> <Button type="submit" variant="primary" size="large" disabled={store.saving}> - {store.saving ? "Saving..." : "Save"} + {store.saving ? language.t("common.saving") : language.t("common.save")} </Button> </div> </form> diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 472a1994f..c4c52fc4d 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" +import { useLanguage } from "@/context/language" interface ForkableMessage { id: string @@ -27,6 +28,7 @@ export const DialogFork: Component = () => { const sdk = useSDK() const prompt = usePrompt() const dialog = useDialog() + const language = useLanguage() const messages = createMemo((): ForkableMessage[] => { const sessionID = params.id @@ -73,11 +75,11 @@ export const DialogFork: Component = () => { } return ( - <Dialog title="Fork from message"> + <Dialog title={language.t("command.session.fork")}> <List class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0" - search={{ placeholder: "Search", autofocus: true }} - emptyMessage="No messages to fork from" + search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.fork.empty")} key={(x) => x.id} items={messages} filterKeys={["text"]} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 66d125288..1ecefa2cb 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -4,14 +4,16 @@ import { Switch } from "@opencode-ai/ui/switch" import type { Component } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" +import { useLanguage } from "@/context/language" export const DialogManageModels: Component = () => { const local = useLocal() + const language = useLanguage() return ( - <Dialog title="Manage models" description="Customize which models appear in the model selector."> + <Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}> <List - search={{ placeholder: "Search models", autofocus: true }} - emptyMessage="No model results" + search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.model.empty")} key={(x) => `${x?.provider?.id}:${x?.id}`} items={local.model.list()} filterKeys={["provider.name", "name", "id"]} diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index bf4a1f9ed..1ee2501de 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -6,6 +6,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path" import { createMemo } from "solid-js" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" interface DialogSelectDirectoryProps { title?: string @@ -17,6 +18,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const sync = useGlobalSync() const sdk = useGlobalSDK() const dialog = useDialog() + const language = useLanguage() const home = createMemo(() => sync.data.path.home) const root = createMemo(() => sync.data.path.home || sync.data.path.directory) @@ -81,10 +83,11 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { } return ( - <Dialog title={props.title ?? "Open project"}> + <Dialog title={props.title ?? language.t("command.project.open")}> <List - search={{ placeholder: "Search folders", autofocus: true }} - emptyMessage="No folders found" + search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.directory.empty")} + loadingMessage={language.t("common.loading")} items={directories} key={(x) => x} onSelect={(path) => { diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 2e28c4d2e..7c3113a54 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -9,6 +9,7 @@ import { createMemo, createSignal, onCleanup, Show } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" type EntryType = "command" | "file" @@ -18,13 +19,14 @@ type Entry = { title: string description?: string keybind?: string - category: "Commands" | "Files" + category: string option?: CommandOption path?: string } export function DialogSelectFile() { const command = useCommand() + const language = useLanguage() const layout = useLayout() const file = useFile() const dialog = useDialog() @@ -56,7 +58,7 @@ export function DialogSelectFile() { title: option.title, description: option.description, keybind: option.keybind, - category: "Commands", + category: language.t("palette.group.commands"), option, }) @@ -64,7 +66,7 @@ export function DialogSelectFile() { id: "file:" + path, type: "file", title: path, - category: "Files", + category: language.t("palette.group.files"), path, }) @@ -143,8 +145,14 @@ export function DialogSelectFile() { return ( <Dialog class="pt-3 pb-0 !max-h-[480px]"> <List - search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }} - emptyMessage="No results found" + search={{ + placeholder: language.t("palette.search.placeholder"), + autofocus: true, + hideIcon: true, + class: "pl-3 pr-2 !mb-0", + }} + emptyMessage={language.t("palette.empty")} + loadingMessage={language.t("common.loading")} items={items} key={(item) => item.id} filterKeys={["title", "description", "category"]} diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index c29cd827e..25ef8df01 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -4,10 +4,12 @@ import { useSDK } from "@/context/sdk" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { useLanguage } from "@/context/language" export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() + const language = useLanguage() const [loading, setLoading] = createSignal<string | null>(null) const items = createMemo(() => @@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => { const totalCount = createMemo(() => items().length) return ( - <Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}> + <Dialog + title={language.t("dialog.mcp.title")} + description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })} + > <List - search={{ placeholder: "Search", autofocus: true }} - emptyMessage="No MCPs configured" + search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.mcp.empty")} key={(x) => x?.name ?? ""} items={items} filterKeys={["name", "status"]} @@ -60,16 +65,16 @@ export const DialogSelectMcp: Component = () => { <div class="flex items-center gap-2"> <span class="truncate">{i.name}</span> <Show when={status() === "connected"}> - <span class="text-11-regular text-text-weaker">connected</span> + <span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span> </Show> <Show when={status() === "failed"}> - <span class="text-11-regular text-text-weaker">failed</span> + <span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span> </Show> <Show when={status() === "needs_auth"}> - <span class="text-11-regular text-text-weaker">needs auth</span> + <span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span> </Show> <Show when={status() === "disabled"}> - <span class="text-11-regular text-text-weaker">disabled</span> + <span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span> </Show> <Show when={loading() === i.name}> <span class="text-11-regular text-text-weak">...</span> diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 24ec8092d..6ac1fa678 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -10,11 +10,13 @@ import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" +import { useLanguage } from "@/context/language" export const DialogSelectModelUnpaid: Component = () => { const local = useLocal() const dialog = useDialog() const providers = useProviders() + const language = useLanguage() let listRef: ListRef | undefined const handleKey = (e: KeyboardEvent) => { @@ -30,9 +32,9 @@ export const DialogSelectModelUnpaid: Component = () => { }) return ( - <Dialog title="Select model"> + <Dialog title={language.t("dialog.model.select.title")}> <div class="flex flex-col gap-3 px-2.5"> - <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div> + <div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div> <List ref={(ref) => (listRef = ref)} items={local.model.list} @@ -48,9 +50,9 @@ export const DialogSelectModelUnpaid: Component = () => { {(i) => ( <div class="w-full flex items-center gap-x-2.5"> <span>{i.name}</span> - <Tag>Free</Tag> + <Tag>{language.t("model.tag.free")}</Tag> <Show when={i.latest}> - <Tag>Latest</Tag> + <Tag>{language.t("model.tag.latest")}</Tag> </Show> </div> )} @@ -60,9 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => { </div> <div class="px-1.5 pb-1.5"> <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base"> - <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4"> - <div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div> - <div class="w-full"> + <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4"> + <div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div> + <div class="w-full"> <List class="w-full px-0" key={(x) => x?.id} @@ -83,10 +85,10 @@ export const DialogSelectModelUnpaid: Component = () => { <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} /> <span>{i.name}</span> <Show when={i.id === "opencode"}> - <Tag>Recommended</Tag> + <Tag>{language.t("dialog.provider.tag.recommended")}</Tag> </Show> <Show when={i.id === "anthropic"}> - <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div> + <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div> </Show> </div> )} @@ -99,7 +101,7 @@ export const DialogSelectModelUnpaid: Component = () => { dialog.show(() => <DialogSelectProvider />) }} > - View all providers + {language.t("dialog.provider.viewAll")} </Button> </div> </div> diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index d54f9369a..ba42ffdd6 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -9,6 +9,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogManageModels } from "./dialog-manage-models" +import { useLanguage } from "@/context/language" const ModelList: Component<{ provider?: string @@ -16,6 +17,7 @@ const ModelList: Component<{ onSelect: () => void }> = (props) => { const local = useLocal() + const language = useLanguage() const models = createMemo(() => local.model @@ -27,8 +29,8 @@ const ModelList: Component<{ return ( <List class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`} - search={{ placeholder: "Search models", autofocus: true }} - emptyMessage="No model results" + search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.model.empty")} key={(x) => `${x.provider.id}:${x.id}`} items={models} current={local.model.current()} @@ -55,10 +57,10 @@ const ModelList: Component<{ <div class="w-full flex items-center gap-x-2 text-13-regular"> <span class="truncate">{i.name}</span> <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}> - <Tag>Free</Tag> + <Tag>{language.t("model.tag.free")}</Tag> </Show> <Show when={i.latest}> - <Tag>Latest</Tag> + <Tag>{language.t("model.tag.latest")}</Tag> </Show> </div> )} @@ -71,13 +73,14 @@ export const ModelSelectorPopover: Component<{ children: JSX.Element }> = (props) => { const [open, setOpen] = createSignal(false) + const language = useLanguage() return ( <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}> <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger> <Kobalte.Portal> <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"> - <Kobalte.Title class="sr-only">Select model</Kobalte.Title> + <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title> <ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" /> </Kobalte.Content> </Kobalte.Portal> @@ -87,10 +90,11 @@ export const ModelSelectorPopover: Component<{ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { const dialog = useDialog() + const language = useLanguage() return ( <Dialog - title="Select model" + title={language.t("dialog.model.select.title")} action={ <Button class="h-7 -my-1 text-14-medium" @@ -98,7 +102,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { tabIndex={-1} onClick={() => dialog.show(() => <DialogSelectProvider />)} > - Connect provider + {language.t("command.provider.connect")} </Button> } > @@ -108,7 +112,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={() => dialog.show(() => <DialogManageModels />)} > - Manage models + {language.t("dialog.model.manage")} </Button> </Dialog> ) diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 5bbde5d41..1e059c219 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -7,28 +7,38 @@ import { Tag } from "@opencode-ai/ui/tag" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" import { DialogConnectProvider } from "./dialog-connect-provider" +import { useLanguage } from "@/context/language" export const DialogSelectProvider: Component = () => { const dialog = useDialog() const providers = useProviders() + const language = useLanguage() + + const popularGroup = () => language.t("dialog.provider.group.popular") + const otherGroup = () => language.t("dialog.provider.group.other") return ( - <Dialog title="Connect provider"> + <Dialog title={language.t("command.provider.connect")}> <List - search={{ placeholder: "Search providers", autofocus: true }} + search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.provider.empty")} activeIcon="plus-small" key={(x) => x?.id} - items={providers.all} + items={() => { + language.locale() + return providers.all() + }} filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} sortBy={(a, b) => { if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return a.name.localeCompare(b.name) }} sortGroupsBy={(a, b) => { - if (a.category === "Popular" && b.category !== "Popular") return -1 - if (b.category === "Popular" && a.category !== "Popular") return 1 + const popular = popularGroup() + if (a.category === popular && b.category !== popular) return -1 + if (b.category === popular && a.category !== popular) return 1 return 0 }} onSelect={(x) => { @@ -41,10 +51,10 @@ export const DialogSelectProvider: Component = () => { <ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} /> <span>{i.name}</span> <Show when={i.id === "opencode"}> - <Tag>Recommended</Tag> + <Tag>{language.t("dialog.provider.tag.recommended")}</Tag> </Show> <Show when={i.id === "anthropic"}> - <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div> + <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div> </Show> </div> )} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 90f372128..0b3967b76 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -10,6 +10,7 @@ import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/serv import { usePlatform } from "@/context/platform" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { useNavigate } from "@solidjs/router" +import { useLanguage } from "@/context/language" type ServerStatus = { healthy: boolean; version?: string } @@ -30,6 +31,7 @@ export function DialogSelectServer() { const dialog = useDialog() const server = useServer() const platform = usePlatform() + const language = useLanguage() const [store, setStore] = createStore({ url: "", adding: false, @@ -109,7 +111,7 @@ export function DialogSelectServer() { setStore("adding", false) if (!result.healthy) { - setStore("error", "Could not connect to server") + setStore("error", language.t("dialog.server.add.error")) return } @@ -122,11 +124,11 @@ export function DialogSelectServer() { } return ( - <Dialog title="Servers" description="Switch which OpenCode server this app connects to."> + <Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}> <div class="flex flex-col gap-4 pb-4"> <List - search={{ placeholder: "Search servers", autofocus: true }} - emptyMessage="No servers yet" + search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }} + emptyMessage={language.t("dialog.server.empty")} items={sortedItems} key={(x) => x} current={current()} @@ -168,14 +170,14 @@ export function DialogSelectServer() { <div class="mt-6 px-3 flex flex-col gap-1.5"> <div class="px-3"> - <h3 class="text-14-regular text-text-weak">Add a server</h3> + <h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3> </div> <form onSubmit={handleSubmit}> <div class="flex items-start gap-2"> <div class="flex-1 min-w-0 h-auto"> <TextField type="text" - label="Server URL" + label={language.t("dialog.server.add.url")} hideLabel placeholder="http://localhost:4096" value={store.url} @@ -188,7 +190,7 @@ export function DialogSelectServer() { /> </div> <Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}> - {store.adding ? "Checking..." : "Add"} + {store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")} </Button> </div> </form> @@ -197,9 +199,9 @@ export function DialogSelectServer() { <Show when={isDesktop}> <div class="mt-6 px-3 flex flex-col gap-1.5"> <div class="px-3"> - <h3 class="text-14-regular text-text-weak">Default server</h3> + <h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3> <p class="text-12-regular text-text-weak mt-1"> - Connect to this server on app launch instead of starting a local server. Requires restart. + {language.t("dialog.server.default.description")} </p> </div> <div class="flex items-center gap-2 px-3 py-2"> @@ -208,7 +210,7 @@ export function DialogSelectServer() { fallback={ <Show when={server.url} - fallback={<span class="text-14-regular text-text-weak">No server selected</span>} + fallback={<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>} > <Button variant="secondary" @@ -218,7 +220,7 @@ export function DialogSelectServer() { defaultUrlActions.refetch(server.url) }} > - Set current server as default + {language.t("dialog.server.default.set")} </Button> </Show> } @@ -234,7 +236,7 @@ export function DialogSelectServer() { defaultUrlActions.refetch() }} > - Clear + {language.t("dialog.server.default.clear")} </Button> </Show> </div> diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 072ef0bdd..63e9dfbfb 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -49,6 +49,7 @@ import { Persist, persisted } from "@/utils/persist" import { Identifier } from "@/utils/id" import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" +import { useLanguage } from "@/context/language" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client" @@ -118,6 +119,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const providers = useProviders() const command = useCommand() const permission = usePermission() + const language = useLanguage() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement let scrollRef!: HTMLDivElement @@ -1560,8 +1562,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Show when={!prompt.dirty()}> <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" - ? "Enter shell command..." - : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} + ? language.t("prompt.placeholder.shell") + : language.t("prompt.placeholder.normal", { example: PLACEHOLDERS[store.placeholder] })} </div> </Show> </div> @@ -1571,12 +1573,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Match when={store.mode === "shell"}> <div class="flex items-center gap-2 px-2 h-6"> <Icon name="console" size="small" class="text-icon-primary" /> - <span class="text-12-regular text-text-primary">Shell</span> - <span class="text-12-regular text-text-weak">esc to exit</span> + <span class="text-12-regular text-text-primary">{language.t("prompt.mode.shell")}</span> + <span class="text-12-regular text-text-weak">{language.t("prompt.mode.shell.exit")}</span> </div> </Match> <Match when={store.mode === "normal"}> - <TooltipKeybind placement="top" title="Cycle agent" keybind={command.keybind("agent.cycle")}> + <TooltipKeybind + placement="top" + title={language.t("command.agent.cycle")} + keybind={command.keybind("agent.cycle")} + > <Select options={local.agent.list().map((agent) => agent.name)} current={local.agent.current()?.name ?? ""} @@ -1588,24 +1594,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Show when={providers.paid().length > 0} fallback={ - <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}> + <TooltipKeybind + placement="top" + title={language.t("command.model.choose")} + keybind={command.keybind("model.choose")} + > <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}> <Show when={local.model.current()?.provider?.id}> <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" /> </Show> - {local.model.current()?.name ?? "Select model"} + {local.model.current()?.name ?? language.t("dialog.model.select.title")} <Icon name="chevron-down" size="small" /> </Button> </TooltipKeybind> } > <ModelSelectorPopover> - <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}> + <TooltipKeybind + placement="top" + title={language.t("command.model.choose")} + keybind={command.keybind("model.choose")} + > <Button as="div" variant="ghost"> <Show when={local.model.current()?.provider?.id}> <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" /> </Show> - {local.model.current()?.name ?? "Select model"} + {local.model.current()?.name ?? language.t("dialog.model.select.title")} <Icon name="chevron-down" size="small" /> </Button> </TooltipKeybind> @@ -1614,7 +1628,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Show when={local.model.variant.list().length > 0}> <TooltipKeybind placement="top" - title="Thinking effort" + title={language.t("command.model.variant.cycle")} keybind={command.keybind("model.variant.cycle")} > <Button @@ -1622,14 +1636,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => { class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular" onClick={() => local.model.variant.cycle()} > - {local.model.variant.current() ?? "Default"} + {local.model.variant.current() ?? language.t("common.default")} </Button> </TooltipKeybind> </Show> <Show when={permission.permissionsEnabled() && params.id}> <TooltipKeybind placement="top" - title="Auto-accept edits" + title={language.t("command.permissions.autoaccept.enable")} keybind={command.keybind("permissions.autoaccept")} > <Button diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index a975f9fa5..030ae6d58 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -11,6 +11,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { Code } from "@opencode-ai/ui/code" import { Markdown } from "@opencode-ai/ui/markdown" import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" +import { useLanguage } from "@/context/language" interface SessionContextTabProps { messages: () => Message[] @@ -22,6 +23,7 @@ interface SessionContextTabProps { export function SessionContextTab(props: SessionContextTabProps) { const params = useParams() const sync = useSync() + const language = useLanguage() const ctx = createMemo(() => { const last = props.messages().findLast((x) => { @@ -172,7 +174,7 @@ export function SessionContextTab(props: SessionContextTabProps) { return [ { key: "system", - label: "System", + label: language.t("context.breakdown.system"), tokens: tokens.system, width: pct(tokens.system), percent: pctLabel(tokens.system), @@ -180,7 +182,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { key: "user", - label: "User", + label: language.t("context.breakdown.user"), tokens: tokens.user, width: pct(tokens.user), percent: pctLabel(tokens.user), @@ -188,7 +190,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { key: "assistant", - label: "Assistant", + label: language.t("context.breakdown.assistant"), tokens: tokens.assistant, width: pct(tokens.assistant), percent: pctLabel(tokens.assistant), @@ -196,7 +198,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { key: "tool", - label: "Tool Calls", + label: language.t("context.breakdown.tool"), tokens: tokens.tool, width: pct(tokens.tool), percent: pctLabel(tokens.tool), @@ -204,7 +206,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }, { key: "other", - label: "Other", + label: language.t("context.breakdown.other"), tokens: tokens.other, width: pct(tokens.other), percent: pctLabel(tokens.other), @@ -243,22 +245,25 @@ export function SessionContextTab(props: SessionContextTabProps) { const c = ctx() const count = counts() return [ - { label: "Session", value: props.info()?.title ?? params.id ?? "—" }, - { label: "Messages", value: count.all.toLocaleString() }, - { label: "Provider", value: providerLabel() }, - { label: "Model", value: modelLabel() }, - { label: "Context Limit", value: number(c?.limit) }, - { label: "Total Tokens", value: number(c?.total) }, - { label: "Usage", value: percent(c?.usage) }, - { label: "Input Tokens", value: number(c?.input) }, - { label: "Output Tokens", value: number(c?.output) }, - { label: "Reasoning Tokens", value: number(c?.reasoning) }, - { label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` }, - { label: "User Messages", value: count.user.toLocaleString() }, - { label: "Assistant Messages", value: count.assistant.toLocaleString() }, - { label: "Total Cost", value: cost() }, - { label: "Session Created", value: time(props.info()?.time.created) }, - { label: "Last Activity", value: time(c?.message.time.created) }, + { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" }, + { label: language.t("context.stats.messages"), value: count.all.toLocaleString() }, + { label: language.t("context.stats.provider"), value: providerLabel() }, + { label: language.t("context.stats.model"), value: modelLabel() }, + { label: language.t("context.stats.limit"), value: number(c?.limit) }, + { label: language.t("context.stats.totalTokens"), value: number(c?.total) }, + { label: language.t("context.stats.usage"), value: percent(c?.usage) }, + { label: language.t("context.stats.inputTokens"), value: number(c?.input) }, + { label: language.t("context.stats.outputTokens"), value: number(c?.output) }, + { label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) }, + { + label: language.t("context.stats.cacheTokens"), + value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`, + }, + { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString() }, + { label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString() }, + { label: language.t("context.stats.totalCost"), value: cost() }, + { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) }, + { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) }, ] satisfies { label: string; value: JSX.Element }[] }) @@ -371,7 +376,7 @@ export function SessionContextTab(props: SessionContextTabProps) { <Show when={breakdown().length > 0}> <div class="flex flex-col gap-2"> - <div class="text-12-regular text-text-weak">Context Breakdown</div> + <div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div> <div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex"> <For each={breakdown()}> {(segment) => ( @@ -397,7 +402,7 @@ export function SessionContextTab(props: SessionContextTabProps) { </For> </div> <div class="hidden text-11-regular text-text-weaker"> - Approximate breakdown of input tokens. "Other" includes tool definitions and overhead. + {language.t("context.breakdown.note")} </div> </div> </Show> @@ -405,7 +410,7 @@ export function SessionContextTab(props: SessionContextTabProps) { <Show when={systemPrompt()}> {(prompt) => ( <div class="flex flex-col gap-2"> - <div class="text-12-regular text-text-weak">System Prompt</div> + <div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div> <div class="border border-border-base rounded-md bg-surface-base px-3 py-2"> <Markdown text={prompt()} class="text-12-regular" /> </div> @@ -414,7 +419,7 @@ export function SessionContextTab(props: SessionContextTabProps) { </Show> <div class="flex flex-col gap-2"> - <div class="text-12-regular text-text-weak">Raw messages</div> + <div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div> <Accordion multiple> <For each={props.messages()}>{(message) => <RawMessage message={message} />}</For> </Accordion> diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 681dcb235..39bd11a1b 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -2,6 +2,7 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount, type Access import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { Persist, persisted } from "@/utils/persist" @@ -154,6 +155,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex init: () => { const dialog = useDialog() const settings = useSettings() + const language = useLanguage() const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([]) const [suspendCount, setSuspendCount] = createSignal(0) @@ -213,7 +215,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex ...suggested.map((x) => ({ ...x, id: SUGGESTED_PREFIX + x.id, - category: "Suggested", + category: language.t("command.category.suggested"), })), ...resolved, ] diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index c51b1a7d7..a5993eaa2 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1,9 +1,205 @@ export const dict = { + "command.category.suggested": "Suggested", + "command.category.view": "View", + "command.category.project": "Project", + "command.category.provider": "Provider", + "command.category.server": "Server", + "command.category.session": "Session", + "command.category.theme": "Theme", "command.category.language": "Language", + "command.category.file": "File", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Permissions", + + "theme.scheme.system": "System", + "theme.scheme.light": "Light", + "theme.scheme.dark": "Dark", + + "command.sidebar.toggle": "Toggle sidebar", + "command.project.open": "Open project", + "command.provider.connect": "Connect provider", + "command.server.switch": "Switch server", + "command.session.previous": "Previous session", + "command.session.next": "Next session", + "command.session.archive": "Archive session", + + "command.theme.cycle": "Cycle theme", + "command.theme.set": "Use theme: {{theme}}", + "command.theme.scheme.cycle": "Cycle color scheme", + "command.theme.scheme.set": "Use color scheme: {{scheme}}", + "command.language.cycle": "Cycle language", "command.language.set": "Use language: {{language}}", + + "command.session.new": "New session", + "command.file.open": "Open file", + "command.file.open.description": "Search files and commands", + "command.terminal.toggle": "Toggle terminal", + "command.review.toggle": "Toggle review", + "command.terminal.new": "New terminal", + "command.terminal.new.description": "Create a new terminal tab", + "command.steps.toggle": "Toggle steps", + "command.steps.toggle.description": "Show or hide steps for the current message", + "command.message.previous": "Previous message", + "command.message.previous.description": "Go to the previous user message", + "command.message.next": "Next message", + "command.message.next.description": "Go to the next user message", + "command.model.choose": "Choose model", + "command.model.choose.description": "Select a different model", + "command.mcp.toggle": "Toggle MCPs", + "command.mcp.toggle.description": "Toggle MCPs", + "command.agent.cycle": "Cycle agent", + "command.agent.cycle.description": "Switch to the next agent", + "command.agent.cycle.reverse": "Cycle agent backwards", + "command.agent.cycle.reverse.description": "Switch to the previous agent", + "command.model.variant.cycle": "Cycle thinking effort", + "command.model.variant.cycle.description": "Switch to the next effort level", + "command.permissions.autoaccept.enable": "Auto-accept edits", + "command.permissions.autoaccept.disable": "Stop auto-accepting edits", + "command.session.undo": "Undo", + "command.session.undo.description": "Undo the last message", + "command.session.redo": "Redo", + "command.session.redo.description": "Redo the last undone message", + "command.session.compact": "Compact session", + "command.session.compact.description": "Summarize the session to reduce context size", + "command.session.fork": "Fork from message", + "command.session.fork.description": "Create a new session from a previous message", + "command.session.share": "Share session", + "command.session.share.description": "Share this session and copy the URL to clipboard", + "command.session.unshare": "Unshare session", + "command.session.unshare.description": "Stop sharing this session", + + "palette.search.placeholder": "Search files and commands", + "palette.empty": "No results found", + "palette.group.commands": "Commands", + "palette.group.files": "Files", + + "dialog.provider.search.placeholder": "Search providers", + "dialog.provider.empty": "No providers found", + "dialog.provider.group.popular": "Popular", + "dialog.provider.group.other": "Other", + "dialog.provider.tag.recommended": "Recommended", + "dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key", + + "dialog.model.select.title": "Select model", + "dialog.model.search.placeholder": "Search models", + "dialog.model.empty": "No model results", + "dialog.model.manage": "Manage models", + "dialog.model.manage.description": "Customize which models appear in the model selector.", + + "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", + "dialog.model.unpaid.addMore.title": "Add more models from popular providers", + + "dialog.provider.viewAll": "View all providers", + + "model.tag.free": "Free", + "model.tag.latest": "Latest", + + "common.search.placeholder": "Search", + "common.loading": "Loading", + "common.cancel": "Cancel", + "common.save": "Save", + "common.saving": "Saving...", + "common.default": "Default", + + "prompt.placeholder.shell": "Enter shell command...", + "prompt.placeholder.normal": "Ask anything... \"{{example}}\"", + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc to exit", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} of {{total}} enabled", + "dialog.mcp.empty": "No MCPs configured", + + "mcp.status.connected": "connected", + "mcp.status.failed": "failed", + "mcp.status.needs_auth": "needs auth", + "mcp.status.disabled": "disabled", + + "dialog.fork.empty": "No messages to fork from", + + "dialog.directory.search.placeholder": "Search folders", + "dialog.directory.empty": "No folders found", + + "dialog.server.title": "Servers", + "dialog.server.description": "Switch which OpenCode server this app connects to.", + "dialog.server.search.placeholder": "Search servers", + "dialog.server.empty": "No servers yet", + "dialog.server.add.title": "Add a server", + "dialog.server.add.url": "Server URL", + "dialog.server.add.error": "Could not connect to server", + "dialog.server.add.checking": "Checking...", + "dialog.server.add.button": "Add", + "dialog.server.default.title": "Default server", + "dialog.server.default.description": "Connect to this server on app launch instead of starting a local server. Requires restart.", + "dialog.server.default.none": "No server selected", + "dialog.server.default.set": "Set current server as default", + "dialog.server.default.clear": "Clear", + + "dialog.project.edit.title": "Edit project", + "dialog.project.edit.name": "Name", + "dialog.project.edit.icon": "Icon", + "dialog.project.edit.icon.alt": "Project icon", + "dialog.project.edit.icon.hint": "Click or drag an image", + "dialog.project.edit.icon.recommended": "Recommended: 128x128px", + "dialog.project.edit.color": "Color", + + "context.breakdown.title": "Context Breakdown", + "context.breakdown.note": "Approximate breakdown of input tokens. \"Other\" includes tool definitions and overhead.", + "context.breakdown.system": "System", + "context.breakdown.user": "User", + "context.breakdown.assistant": "Assistant", + "context.breakdown.tool": "Tool Calls", + "context.breakdown.other": "Other", + + "context.systemPrompt.title": "System Prompt", + "context.rawMessages.title": "Raw messages", + + "context.stats.session": "Session", + "context.stats.messages": "Messages", + "context.stats.provider": "Provider", + "context.stats.model": "Model", + "context.stats.limit": "Context Limit", + "context.stats.totalTokens": "Total Tokens", + "context.stats.usage": "Usage", + "context.stats.inputTokens": "Input Tokens", + "context.stats.outputTokens": "Output Tokens", + "context.stats.reasoningTokens": "Reasoning Tokens", + "context.stats.cacheTokens": "Cache Tokens (read/write)", + "context.stats.userMessages": "User Messages", + "context.stats.assistantMessages": "Assistant Messages", + "context.stats.totalCost": "Total Cost", + "context.stats.sessionCreated": "Session Created", + "context.stats.lastActivity": "Last Activity", + "language.en": "English", "language.zh": "Chinese", + "toast.language.title": "Language", "toast.language.description": "Switched to {{language}}", + + "toast.theme.title": "Theme switched", + "toast.scheme.title": "Color scheme", + + "toast.permissions.autoaccept.on.title": "Auto-accepting edits", + "toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved", + "toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits", + "toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval", + + "toast.model.none.title": "No model selected", + "toast.model.none.description": "Connect a provider to summarize this session", + + "toast.session.share.copyFailed.title": "Failed to copy URL to clipboard", + "toast.session.share.success.title": "Session shared", + "toast.session.share.success.description": "Share URL copied to clipboard!", + "toast.session.share.failed.title": "Failed to share session", + "toast.session.share.failed.description": "An error occurred while sharing the session", + + "toast.session.unshare.success.title": "Session unshared", + "toast.session.unshare.success.description": "Session unshared successfully!", + "toast.session.unshare.failed.title": "Failed to unshare session", + "toast.session.unshare.failed.description": "An error occurred while unsharing the session", } diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 3f1360821..5d9a070c4 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -3,11 +3,207 @@ import { dict as en } from "./en" type Keys = keyof typeof en export const dict = { - "command.category.language": "\u8bed\u8a00", - "command.language.cycle": "\u5207\u6362\u8bed\u8a00", - "command.language.set": "\u4f7f\u7528\u8bed\u8a00: {{language}}", - "language.en": "\u82f1\u8bed", - "language.zh": "\u4e2d\u6587", - "toast.language.title": "\u8bed\u8a00", - "toast.language.description": "\u5df2\u5207\u6362\u5230{{language}}", + "command.category.suggested": "建议", + "command.category.view": "视图", + "command.category.project": "项目", + "command.category.provider": "提供商", + "command.category.server": "服务器", + "command.category.session": "会话", + "command.category.theme": "主题", + "command.category.language": "语言", + "command.category.file": "文件", + "command.category.terminal": "终端", + "command.category.model": "模型", + "command.category.mcp": "MCP", + "command.category.agent": "智能体", + "command.category.permissions": "权限", + + "theme.scheme.system": "系统", + "theme.scheme.light": "浅色", + "theme.scheme.dark": "深色", + + "command.sidebar.toggle": "切换侧边栏", + "command.project.open": "打开项目", + "command.provider.connect": "连接提供商", + "command.server.switch": "切换服务器", + "command.session.previous": "上一个会话", + "command.session.next": "下一个会话", + "command.session.archive": "归档会话", + + "command.theme.cycle": "切换主题", + "command.theme.set": "使用主题: {{theme}}", + "command.theme.scheme.cycle": "切换配色方案", + "command.theme.scheme.set": "使用配色方案: {{scheme}}", + + "command.language.cycle": "切换语言", + "command.language.set": "使用语言: {{language}}", + + "command.session.new": "新建会话", + "command.file.open": "打开文件", + "command.file.open.description": "搜索文件和命令", + "command.terminal.toggle": "切换终端", + "command.review.toggle": "切换审查", + "command.terminal.new": "新建终端", + "command.terminal.new.description": "创建新的终端标签页", + "command.steps.toggle": "切换步骤", + "command.steps.toggle.description": "显示或隐藏当前消息的步骤", + "command.message.previous": "上一条消息", + "command.message.previous.description": "跳转到上一条用户消息", + "command.message.next": "下一条消息", + "command.message.next.description": "跳转到下一条用户消息", + "command.model.choose": "选择模型", + "command.model.choose.description": "选择不同的模型", + "command.mcp.toggle": "切换 MCPs", + "command.mcp.toggle.description": "切换 MCPs", + "command.agent.cycle": "切换智能体", + "command.agent.cycle.description": "切换到下一个智能体", + "command.agent.cycle.reverse": "反向切换智能体", + "command.agent.cycle.reverse.description": "切换到上一个智能体", + "command.model.variant.cycle": "切换思考强度", + "command.model.variant.cycle.description": "切换到下一个强度等级", + "command.permissions.autoaccept.enable": "自动接受编辑", + "command.permissions.autoaccept.disable": "停止自动接受编辑", + "command.session.undo": "撤销", + "command.session.undo.description": "撤销上一条消息", + "command.session.redo": "重做", + "command.session.redo.description": "重做上一条撤销的消息", + "command.session.compact": "精简会话", + "command.session.compact.description": "总结会话以减少上下文大小", + "command.session.fork": "从消息分叉", + "command.session.fork.description": "从之前的消息创建新会话", + "command.session.share": "分享会话", + "command.session.share.description": "分享此会话并将链接复制到剪贴板", + "command.session.unshare": "取消分享会话", + "command.session.unshare.description": "停止分享此会话", + + "palette.search.placeholder": "搜索文件和命令", + "palette.empty": "未找到结果", + "palette.group.commands": "命令", + "palette.group.files": "文件", + + "dialog.provider.search.placeholder": "搜索提供商", + "dialog.provider.empty": "未找到提供商", + "dialog.provider.group.popular": "热门", + "dialog.provider.group.other": "其他", + "dialog.provider.tag.recommended": "推荐", + "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", + + "dialog.model.select.title": "选择模型", + "dialog.model.search.placeholder": "搜索模型", + "dialog.model.empty": "未找到模型", + "dialog.model.manage": "管理模型", + "dialog.model.manage.description": "自定义模型选择器中显示的模型。", + + "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", + "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", + + "dialog.provider.viewAll": "查看全部提供商", + + "model.tag.free": "免费", + "model.tag.latest": "最新", + + "common.search.placeholder": "搜索", + "common.loading": "加载中", + "common.cancel": "取消", + "common.save": "保存", + "common.saving": "保存中...", + "common.default": "默认", + + "prompt.placeholder.shell": "输入 shell 命令...", + "prompt.placeholder.normal": "随便问点什么... \"{{example}}\"", + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "按 esc 退出", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", + "dialog.mcp.empty": "未配置 MCPs", + + "mcp.status.connected": "已连接", + "mcp.status.failed": "失败", + "mcp.status.needs_auth": "需要授权", + "mcp.status.disabled": "已禁用", + + "dialog.fork.empty": "没有可用于分叉的消息", + + "dialog.directory.search.placeholder": "搜索文件夹", + "dialog.directory.empty": "未找到文件夹", + + "dialog.server.title": "服务器", + "dialog.server.description": "切换此应用连接的 OpenCode 服务器。", + "dialog.server.search.placeholder": "搜索服务器", + "dialog.server.empty": "暂无服务器", + "dialog.server.add.title": "添加服务器", + "dialog.server.add.url": "服务器 URL", + "dialog.server.add.error": "无法连接到服务器", + "dialog.server.add.checking": "检查中...", + "dialog.server.add.button": "添加", + "dialog.server.default.title": "默认服务器", + "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。", + "dialog.server.default.none": "未选择服务器", + "dialog.server.default.set": "将当前服务器设为默认", + "dialog.server.default.clear": "清除", + + "dialog.project.edit.title": "编辑项目", + "dialog.project.edit.name": "名称", + "dialog.project.edit.icon": "图标", + "dialog.project.edit.icon.alt": "项目图标", + "dialog.project.edit.icon.hint": "点击或拖拽图片", + "dialog.project.edit.icon.recommended": "建议:128x128px", + "dialog.project.edit.color": "颜色", + + "context.breakdown.title": "上下文拆分", + "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", + "context.breakdown.system": "系统", + "context.breakdown.user": "用户", + "context.breakdown.assistant": "助手", + "context.breakdown.tool": "工具调用", + "context.breakdown.other": "其他", + + "context.systemPrompt.title": "系统提示词", + "context.rawMessages.title": "原始消息", + + "context.stats.session": "会话", + "context.stats.messages": "消息数", + "context.stats.provider": "提供商", + "context.stats.model": "模型", + "context.stats.limit": "上下文限制", + "context.stats.totalTokens": "总 token", + "context.stats.usage": "使用率", + "context.stats.inputTokens": "输入 token", + "context.stats.outputTokens": "输出 token", + "context.stats.reasoningTokens": "推理 token", + "context.stats.cacheTokens": "缓存 token(读/写)", + "context.stats.userMessages": "用户消息", + "context.stats.assistantMessages": "助手消息", + "context.stats.totalCost": "总成本", + "context.stats.sessionCreated": "创建时间", + "context.stats.lastActivity": "最后活动", + + "language.en": "英语", + "language.zh": "中文", + + "toast.language.title": "语言", + "toast.language.description": "已切换到{{language}}", + + "toast.theme.title": "主题已切换", + "toast.scheme.title": "配色方案", + + "toast.permissions.autoaccept.on.title": "自动接受编辑", + "toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批", + "toast.permissions.autoaccept.off.title": "已停止自动接受编辑", + "toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准", + + "toast.model.none.title": "未选择模型", + "toast.model.none.description": "请先连接提供商以总结此会话", + + "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", + "toast.session.share.success.title": "会话已分享", + "toast.session.share.success.description": "分享链接已复制到剪贴板", + "toast.session.share.failed.title": "分享会话失败", + "toast.session.share.failed.description": "分享会话时发生错误", + + "toast.session.unshare.success.title": "已取消分享会话", + "toast.session.unshare.success.description": "会话已成功取消分享", + "toast.session.unshare.failed.title": "取消分享失败", + "toast.session.unshare.failed.description": "取消分享会话时发生错误", } satisfies Partial<Record<Keys, string>> diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index bd6c044a8..f5910b88b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -114,11 +114,12 @@ export default function Layout(props: ParentProps) { const initialDir = params.dir const availableThemeEntries = createMemo(() => Object.entries(theme.themes())) const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] - const colorSchemeLabel: Record<ColorScheme, string> = { - system: "System", - light: "Light", - dark: "Dark", + const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = { + system: "theme.scheme.system", + light: "theme.scheme.light", + dark: "theme.scheme.dark", } + const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) const [editor, setEditor] = createStore({ active: "" as string, @@ -252,7 +253,7 @@ export default function Layout(props: ParentProps) { theme.setTheme(nextThemeId) const nextTheme = theme.themes()[nextThemeId] showToast({ - title: "Theme switched", + title: language.t("toast.theme.title"), description: nextTheme?.name ?? nextThemeId, }) } @@ -265,8 +266,8 @@ export default function Layout(props: ParentProps) { const next = colorSchemeOrder[nextIndex] theme.setColorScheme(next) showToast({ - title: "Color scheme", - description: colorSchemeLabel[next], + title: language.t("toast.scheme.title"), + description: colorSchemeLabel(next), }) } @@ -827,28 +828,28 @@ export default function Layout(props: ParentProps) { const commands: CommandOption[] = [ { id: "sidebar.toggle", - title: "Toggle sidebar", - category: "View", + title: language.t("command.sidebar.toggle"), + category: language.t("command.category.view"), keybind: "mod+b", onSelect: () => layout.sidebar.toggle(), }, { id: "project.open", - title: "Open project", - category: "Project", + title: language.t("command.project.open"), + category: language.t("command.category.project"), keybind: "mod+o", onSelect: () => chooseProject(), }, { id: "provider.connect", - title: "Connect provider", - category: "Provider", + title: language.t("command.provider.connect"), + category: language.t("command.category.provider"), onSelect: () => connectProvider(), }, { id: "server.switch", - title: "Switch server", - category: "Server", + title: language.t("command.server.switch"), + category: language.t("command.category.server"), onSelect: () => openServer(), }, { @@ -860,22 +861,22 @@ export default function Layout(props: ParentProps) { }, { id: "session.previous", - title: "Previous session", - category: "Session", + title: language.t("command.session.previous"), + category: language.t("command.category.session"), keybind: "alt+arrowup", onSelect: () => navigateSessionByOffset(-1), }, { id: "session.next", - title: "Next session", - category: "Session", + title: language.t("command.session.next"), + category: language.t("command.category.session"), keybind: "alt+arrowdown", onSelect: () => navigateSessionByOffset(1), }, { id: "session.archive", - title: "Archive session", - category: "Session", + title: language.t("command.session.archive"), + category: language.t("command.category.session"), keybind: "mod+shift+backspace", disabled: !params.dir || !params.id, onSelect: () => { @@ -885,8 +886,8 @@ export default function Layout(props: ParentProps) { }, { id: "theme.cycle", - title: "Cycle theme", - category: "Theme", + title: language.t("command.theme.cycle"), + category: language.t("command.category.theme"), keybind: "mod+shift+t", onSelect: () => cycleTheme(1), }, @@ -895,8 +896,8 @@ export default function Layout(props: ParentProps) { for (const [id, definition] of availableThemeEntries()) { commands.push({ id: `theme.set.${id}`, - title: `Use theme: ${definition.name ?? id}`, - category: "Theme", + title: language.t("command.theme.set", { theme: definition.name ?? id }), + category: language.t("command.category.theme"), onSelect: () => theme.commitPreview(), onHighlight: () => { theme.previewTheme(id) @@ -907,8 +908,8 @@ export default function Layout(props: ParentProps) { commands.push({ id: "theme.scheme.cycle", - title: "Cycle color scheme", - category: "Theme", + title: language.t("command.theme.scheme.cycle"), + category: language.t("command.category.theme"), keybind: "mod+shift+s", onSelect: () => cycleColorScheme(1), }) @@ -916,8 +917,8 @@ export default function Layout(props: ParentProps) { for (const scheme of colorSchemeOrder) { commands.push({ id: `theme.scheme.${scheme}`, - title: `Use color scheme: ${colorSchemeLabel[scheme]}`, - category: "Theme", + title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }), + category: language.t("command.category.theme"), onSelect: () => theme.commitPreview(), onHighlight: () => { theme.previewColorScheme(scheme) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 14f77d63e..8803b063e 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -33,6 +33,7 @@ import { DialogSelectModel } from "@/components/dialog-select-model" import { DialogSelectMcp } from "@/components/dialog-select-mcp" import { DialogFork } from "@/components/dialog-fork" import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" import { useNavigate, useParams } from "@solidjs/router" import { UserMessage } from "@opencode-ai/sdk/v2" import type { FileDiff } from "@opencode-ai/sdk/v2/client" @@ -161,6 +162,7 @@ export default function Page() { const dialog = useDialog() const codeComponent = useCodeComponent() const command = useCommand() + const language = useLanguage() const platform = usePlatform() const params = useParams() const navigate = useNavigate() @@ -433,51 +435,51 @@ export default function Page() { command.register(() => [ { id: "session.new", - title: "New session", - category: "Session", + title: language.t("command.session.new"), + category: language.t("command.category.session"), keybind: "mod+shift+s", slash: "new", onSelect: () => navigate(`/${params.dir}/session`), }, { id: "file.open", - title: "Open file", - description: "Search files and commands", - category: "File", + title: language.t("command.file.open"), + description: language.t("command.file.open.description"), + category: language.t("command.category.file"), keybind: "mod+p", slash: "open", onSelect: () => dialog.show(() => <DialogSelectFile />), }, { id: "terminal.toggle", - title: "Toggle terminal", + title: language.t("command.terminal.toggle"), description: "", - category: "View", + category: language.t("command.category.view"), keybind: "ctrl+`", slash: "terminal", onSelect: () => view().terminal.toggle(), }, { id: "review.toggle", - title: "Toggle review", + title: language.t("command.review.toggle"), description: "", - category: "View", + category: language.t("command.category.view"), keybind: "mod+shift+r", onSelect: () => view().reviewPanel.toggle(), }, { id: "terminal.new", - title: "New terminal", - description: "Create a new terminal tab", - category: "Terminal", + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), + category: language.t("command.category.terminal"), keybind: "ctrl+alt+t", onSelect: () => terminal.new(), }, { id: "steps.toggle", - title: "Toggle steps", - description: "Show or hide steps for the current message", - category: "View", + title: language.t("command.steps.toggle"), + description: language.t("command.steps.toggle.description"), + category: language.t("command.category.view"), keybind: "mod+e", slash: "steps", disabled: !params.id, @@ -489,62 +491,62 @@ export default function Page() { }, { id: "message.previous", - title: "Previous message", - description: "Go to the previous user message", - category: "Session", + title: language.t("command.message.previous"), + description: language.t("command.message.previous.description"), + category: language.t("command.category.session"), keybind: "mod+arrowup", disabled: !params.id, onSelect: () => navigateMessageByOffset(-1), }, { id: "message.next", - title: "Next message", - description: "Go to the next user message", - category: "Session", + title: language.t("command.message.next"), + description: language.t("command.message.next.description"), + category: language.t("command.category.session"), keybind: "mod+arrowdown", disabled: !params.id, onSelect: () => navigateMessageByOffset(1), }, { id: "model.choose", - title: "Choose model", - description: "Select a different model", - category: "Model", + title: language.t("command.model.choose"), + description: language.t("command.model.choose.description"), + category: language.t("command.category.model"), keybind: "mod+'", slash: "model", onSelect: () => dialog.show(() => <DialogSelectModel />), }, { id: "mcp.toggle", - title: "Toggle MCPs", - description: "Toggle MCPs", - category: "MCP", + title: language.t("command.mcp.toggle"), + description: language.t("command.mcp.toggle.description"), + category: language.t("command.category.mcp"), keybind: "mod+;", slash: "mcp", onSelect: () => dialog.show(() => <DialogSelectMcp />), }, { id: "agent.cycle", - title: "Cycle agent", - description: "Switch to the next agent", - category: "Agent", + title: language.t("command.agent.cycle"), + description: language.t("command.agent.cycle.description"), + category: language.t("command.category.agent"), keybind: "mod+.", slash: "agent", onSelect: () => local.agent.move(1), }, { id: "agent.cycle.reverse", - title: "Cycle agent backwards", - description: "Switch to the previous agent", - category: "Agent", + title: language.t("command.agent.cycle.reverse"), + description: language.t("command.agent.cycle.reverse.description"), + category: language.t("command.category.agent"), keybind: "shift+mod+.", onSelect: () => local.agent.move(-1), }, { id: "model.variant.cycle", - title: "Cycle thinking effort", - description: "Switch to the next effort level", - category: "Model", + title: language.t("command.model.variant.cycle"), + description: language.t("command.model.variant.cycle.description"), + category: language.t("command.category.model"), keybind: "shift+mod+d", onSelect: () => { local.model.variant.cycle() @@ -554,30 +556,31 @@ export default function Page() { id: "permissions.autoaccept", title: params.id && permission.isAutoAccepting(params.id, sdk.directory) - ? "Stop auto-accepting edits" - : "Auto-accept edits", - category: "Permissions", + ? language.t("command.permissions.autoaccept.disable") + : language.t("command.permissions.autoaccept.enable"), + category: language.t("command.category.permissions"), keybind: "mod+shift+a", disabled: !params.id || !permission.permissionsEnabled(), onSelect: () => { const sessionID = params.id if (!sessionID) return permission.toggleAutoAccept(sessionID, sdk.directory) + const enabled = permission.isAutoAccepting(sessionID, sdk.directory) showToast({ - title: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Auto-accepting edits" - : "Stopped auto-accepting edits", - description: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Edit and write permissions will be automatically approved" - : "Edit and write permissions will require approval", + title: enabled + ? language.t("toast.permissions.autoaccept.on.title") + : language.t("toast.permissions.autoaccept.off.title"), + description: enabled + ? language.t("toast.permissions.autoaccept.on.description") + : language.t("toast.permissions.autoaccept.off.description"), }) }, }, { id: "session.undo", - title: "Undo", - description: "Undo the last message", - category: "Session", + title: language.t("command.session.undo"), + description: language.t("command.session.undo.description"), + category: language.t("command.category.session"), slash: "undo", disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { @@ -604,9 +607,9 @@ export default function Page() { }, { id: "session.redo", - title: "Redo", - description: "Redo the last undone message", - category: "Session", + title: language.t("command.session.redo"), + description: language.t("command.session.redo.description"), + category: language.t("command.category.session"), slash: "redo", disabled: !params.id || !info()?.revert?.messageID, onSelect: async () => { @@ -633,9 +636,9 @@ export default function Page() { }, { id: "session.compact", - title: "Compact session", - description: "Summarize the session to reduce context size", - category: "Session", + title: language.t("command.session.compact"), + description: language.t("command.session.compact.description"), + category: language.t("command.category.session"), slash: "compact", disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { @@ -644,8 +647,8 @@ export default function Page() { const model = local.model.current() if (!model) { showToast({ - title: "No model selected", - description: "Connect a provider to summarize this session", + title: language.t("toast.model.none.title"), + description: language.t("toast.model.none.description"), }) return } @@ -658,72 +661,72 @@ export default function Page() { }, { id: "session.fork", - title: "Fork from message", - description: "Create a new session from a previous message", - category: "Session", + title: language.t("command.session.fork"), + description: language.t("command.session.fork.description"), + category: language.t("command.category.session"), slash: "fork", disabled: !params.id || visibleUserMessages().length === 0, onSelect: () => dialog.show(() => <DialogFork />), }, ...(sync.data.config.share !== "disabled" ? [ - { - id: "session.share", - title: "Share session", - description: "Share this session and copy the URL to clipboard", - category: "Session", - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { + { + id: "session.share", + title: language.t("command.session.share"), + description: language.t("command.session.share.description"), + category: language.t("command.category.session"), + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { if (!params.id) return await sdk.client.session .share({ sessionID: params.id }) .then((res) => { navigator.clipboard.writeText(res.data!.share!.url).catch(() => showToast({ - title: "Failed to copy URL to clipboard", + title: language.t("toast.session.share.copyFailed.title"), variant: "error", }), ) }) .then(() => showToast({ - title: "Session shared", - description: "Share URL copied to clipboard!", + title: language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), variant: "success", }), ) .catch(() => showToast({ - title: "Failed to share session", - description: "An error occurred while sharing the session", + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), variant: "error", }), ) }, }, - { - id: "session.unshare", - title: "Unshare session", - description: "Stop sharing this session", - category: "Session", - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { + { + id: "session.unshare", + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), + category: language.t("command.category.session"), + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { if (!params.id) return await sdk.client.session .unshare({ sessionID: params.id }) .then(() => showToast({ - title: "Session unshared", - description: "Session unshared successfully!", + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), variant: "success", }), ) .catch(() => showToast({ - title: "Failed to unshare session", - description: "An error occurred while unsharing the session", + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), variant: "error", }), ) |
