diff options
Diffstat (limited to 'packages/app/src/components')
12 files changed, 168 insertions, 104 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> |
