diff options
| author | Adam <[email protected]> | 2026-01-20 07:16:02 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-20 17:58:06 -0600 |
| commit | a68e5a1c17b675ba54e06c09ae76ae555a2aeea3 (patch) | |
| tree | 115159f69d1539ee64fda2dcdbffebd566e082a7 | |
| parent | 92beae14100af23c20afe7b6bc2bb393643d698f (diff) | |
| download | opencode-a68e5a1c17b675ba54e06c09ae76ae555a2aeea3.tar.gz opencode-a68e5a1c17b675ba54e06c09ae76ae555a2aeea3.zip | |
wip(app): i18n
| -rw-r--r-- | packages/app/src/i18n/en.ts | 77 | ||||
| -rw-r--r-- | packages/app/src/i18n/zh.ts | 77 | ||||
| -rw-r--r-- | packages/app/src/pages/home.tsx | 24 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 218 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 88 |
5 files changed, 336 insertions, 148 deletions
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index a5993eaa2..cd900e1c0 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -202,4 +202,81 @@ export const dict = { "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", + + "toast.update.title": "Update available", + "toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.", + "toast.update.action.installRestart": "Install and restart", + "toast.update.action.notYet": "Not yet", + + "notification.permission.title": "Permission required", + "notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission", + "notification.question.title": "Question", + "notification.question.description": "{{sessionTitle}} in {{projectName}} has a question", + "notification.action.goToSession": "Go to session", + + "home.recentProjects": "Recent projects", + "home.empty.title": "No recent projects", + "home.empty.description": "Get started by opening a local project", + + "session.tab.session": "Session", + "session.tab.review": "Review", + "session.tab.context": "Context", + "session.review.filesChanged": "{{count}} Files Changed", + "session.review.loadingChanges": "Loading changes...", + "session.review.empty": "No changes in this session yet", + "session.messages.renderEarlier": "Render earlier messages", + "session.messages.loadingEarlier": "Loading earlier messages...", + "session.messages.loadEarlier": "Load earlier messages", + "session.messages.loading": "Loading messages...", + + "session.context.addToContext": "Add {{selection}} to context", + + "prompt.loading": "Loading prompt...", + "terminal.loading": "Loading terminal...", + + "common.closeTab": "Close tab", + "common.dismiss": "Dismiss", + "common.requestFailed": "Request failed", + "common.moreOptions": "More options", + "common.rename": "Rename", + "common.reset": "Reset", + "common.delete": "Delete", + "common.close": "Close", + "common.edit": "Edit", + "common.loadMore": "Load more", + + "sidebar.settings": "Settings", + "sidebar.help": "Help", + "sidebar.workspaces.enable": "Enable workspaces", + "sidebar.workspaces.disable": "Disable workspaces", + "sidebar.gettingStarted.title": "Getting started", + "sidebar.gettingStarted.line1": "OpenCode includes free models so you can start immediately.", + "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Recent sessions", + "sidebar.project.viewAllSessions": "View all sessions", + + "workspace.new": "New workspace", + "workspace.type.local": "local", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Failed to create workspace", + "workspace.delete.failed.title": "Failed to delete workspace", + "workspace.resetting.title": "Resetting workspace", + "workspace.resetting.description": "This may take a minute.", + "workspace.reset.failed.title": "Failed to reset workspace", + "workspace.reset.success.title": "Workspace reset", + "workspace.reset.success.description": "Workspace now matches the default branch.", + "workspace.status.checking": "Checking for unmerged changes...", + "workspace.status.error": "Unable to verify git status.", + "workspace.status.clean": "No unmerged changes detected.", + "workspace.status.dirty": "Unmerged changes detected in this workspace.", + "workspace.delete.title": "Delete workspace", + "workspace.delete.confirm": "Delete workspace \"{{name}}\"?", + "workspace.delete.button": "Delete workspace", + "workspace.reset.title": "Reset workspace", + "workspace.reset.confirm": "Reset workspace \"{{name}}\"?", + "workspace.reset.button": "Reset workspace", + "workspace.reset.archived.none": "No active sessions will be archived.", + "workspace.reset.archived.one": "1 session will be archived.", + "workspace.reset.archived.many": "{{count}} sessions will be archived.", + "workspace.reset.note": "This will reset the workspace to match the default branch.", } diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 5d9a070c4..e8b37fd63 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -206,4 +206,81 @@ export const dict = { "toast.session.unshare.success.description": "会话已成功取消分享", "toast.session.unshare.failed.title": "取消分享失败", "toast.session.unshare.failed.description": "取消分享会话时发生错误", + + "toast.update.title": "有可用更新", + "toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。", + "toast.update.action.installRestart": "安装并重启", + "toast.update.action.notYet": "稍后", + + "notification.permission.title": "需要权限", + "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限", + "notification.question.title": "问题", + "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", + "notification.action.goToSession": "前往会话", + + "home.recentProjects": "最近项目", + "home.empty.title": "没有最近项目", + "home.empty.description": "通过打开本地项目开始使用", + + "session.tab.session": "会话", + "session.tab.review": "审查", + "session.tab.context": "上下文", + "session.review.filesChanged": "{{count}} 个文件变更", + "session.review.loadingChanges": "正在加载更改...", + "session.review.empty": "此会话暂无更改", + "session.messages.renderEarlier": "显示更早的消息", + "session.messages.loadingEarlier": "正在加载更早的消息...", + "session.messages.loadEarlier": "加载更早的消息", + "session.messages.loading": "正在加载消息...", + + "session.context.addToContext": "将 {{selection}} 添加到上下文", + + "prompt.loading": "正在加载提示...", + "terminal.loading": "正在加载终端...", + + "common.closeTab": "关闭标签页", + "common.dismiss": "忽略", + "common.requestFailed": "请求失败", + "common.moreOptions": "更多选项", + "common.rename": "重命名", + "common.reset": "重置", + "common.delete": "删除", + "common.close": "关闭", + "common.edit": "编辑", + "common.loadMore": "加载更多", + + "sidebar.settings": "设置", + "sidebar.help": "帮助", + "sidebar.workspaces.enable": "启用工作区", + "sidebar.workspaces.disable": "禁用工作区", + "sidebar.gettingStarted.title": "入门", + "sidebar.gettingStarted.line1": "OpenCode 提供免费模型,你可以立即开始使用。", + "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", + "sidebar.project.recentSessions": "最近会话", + "sidebar.project.viewAllSessions": "查看全部会话", + + "workspace.new": "新建工作区", + "workspace.type.local": "本地", + "workspace.type.sandbox": "沙盒", + "workspace.create.failed.title": "创建工作区失败", + "workspace.delete.failed.title": "删除工作区失败", + "workspace.resetting.title": "正在重置工作区", + "workspace.resetting.description": "这可能需要一点时间。", + "workspace.reset.failed.title": "重置工作区失败", + "workspace.reset.success.title": "工作区已重置", + "workspace.reset.success.description": "工作区已与默认分支保持一致。", + "workspace.status.checking": "正在检查未合并的更改...", + "workspace.status.error": "无法验证 git 状态。", + "workspace.status.clean": "未检测到未合并的更改。", + "workspace.status.dirty": "检测到未合并的更改。", + "workspace.delete.title": "删除工作区", + "workspace.delete.confirm": "删除工作区 \"{{name}}\"?", + "workspace.delete.button": "删除工作区", + "workspace.reset.title": "重置工作区", + "workspace.reset.confirm": "重置工作区 \"{{name}}\"?", + "workspace.reset.button": "重置工作区", + "workspace.reset.archived.none": "不会归档任何活跃会话。", + "workspace.reset.archived.one": "将归档 1 个会话。", + "workspace.reset.archived.many": "将归档 {{count}} 个会话。", + "workspace.reset.note": "这将把工作区重置为与默认分支一致。", } satisfies Partial<Record<Keys, string>> diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index a9e89a7f5..efed2e658 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -12,6 +12,7 @@ import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" import { useServer } from "@/context/server" import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" export default function Home() { const sync = useGlobalSync() @@ -20,6 +21,7 @@ export default function Home() { const dialog = useDialog() const navigate = useNavigate() const server = useServer() + const language = useLanguage() const homedir = createMemo(() => sync.data.path.home) function openProject(directory: string) { @@ -41,7 +43,7 @@ export default function Home() { if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ - title: "Open project", + title: language.t("command.project.open"), multiple: true, }) resolve(result) @@ -74,13 +76,13 @@ export default function Home() { </Button> <Switch> <Match when={sync.data.project.length > 0}> - <div class="mt-20 w-full flex flex-col gap-4"> - <div class="flex gap-2 items-center justify-between pl-3"> - <div class="text-14-medium text-text-strong">Recent projects</div> - <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}> - Open project - </Button> - </div> + <div class="mt-20 w-full flex flex-col gap-4"> + <div class="flex gap-2 items-center justify-between pl-3"> + <div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div> + <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}> + {language.t("command.project.open")} + </Button> + </div> <ul class="flex flex-col gap-2"> <For each={sync.data.project @@ -108,12 +110,12 @@ export default function Home() { <div class="mt-30 mx-auto flex flex-col items-center gap-3"> <Icon name="folder-add-left" size="large" /> <div class="flex flex-col gap-1 items-center justify-center"> - <div class="text-14-medium text-text-strong">No recent projects</div> - <div class="text-12-regular text-text-weak">Get started by opening a local project</div> + <div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div> + <div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div> </div> <div /> <Button class="px-3" onClick={chooseProject}> - Open project + {language.t("command.project.open")} </Button> </div> </Match> diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f5910b88b..2cbc97a55 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -300,18 +300,18 @@ export default function Layout(props: ParentProps) { toastId = showToast({ persistent: true, icon: "download", - title: "Update available", - description: `A new version of OpenCode (${version}) is now available to install.`, + title: language.t("toast.update.title"), + description: language.t("toast.update.description", { version: version ?? "" }), actions: [ { - label: "Install and restart", + label: language.t("toast.update.action.installRestart"), onClick: async () => { await platform.update!() await platform.restart!() }, }, { - label: "Not yet", + label: language.t("toast.update.action.notYet"), onClick: "dismiss", }, ], @@ -325,27 +325,17 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const alerts = { - "permission.asked": { - title: "Permission required", - icon: "checklist" as const, - description: (sessionTitle: string, projectName: string) => - `${sessionTitle} in ${projectName} needs permission`, - }, - "question.asked": { - title: "Question", - icon: "bubble-5" as const, - description: (sessionTitle: string, projectName: string) => `${sessionTitle} in ${projectName} has a question`, - }, - } - const toastBySession = new Map<string, number>() const alertedAtBySession = new Map<string, number>() const cooldownMs = 5000 const unsub = globalSDK.event.listen((e) => { if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return - const config = alerts[e.details.type] + const title = + e.details.type === "permission.asked" + ? language.t("notification.permission.title") + : language.t("notification.question.title") + const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) const directory = e.name const props = e.details.properties if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return @@ -354,9 +344,12 @@ export default function Layout(props: ParentProps) { const session = store.session.find((s) => s.id === props.sessionID) const sessionKey = `${directory}:${props.sessionID}` - const sessionTitle = session?.title ?? "New session" + const sessionTitle = session?.title ?? language.t("command.session.new") const projectName = getFilename(directory) - const description = config.description(sessionTitle, projectName) + const description = + e.details.type === "permission.asked" + ? language.t("notification.permission.description", { sessionTitle, projectName }) + : language.t("notification.question.description", { sessionTitle, projectName }) const href = `/${base64Encode(directory)}/session/${props.sessionID}` const now = Date.now() @@ -367,13 +360,13 @@ export default function Layout(props: ParentProps) { if (e.details.type === "permission.asked") { playSound(soundSrc(settings.sounds.permissions())) if (settings.notifications.permissions()) { - void platform.notify(config.title, description, href) + void platform.notify(title, description, href) } } if (e.details.type === "question.asked") { if (settings.notifications.agent()) { - void platform.notify(config.title, description, href) + void platform.notify(title, description, href) } } @@ -387,16 +380,16 @@ export default function Layout(props: ParentProps) { const toastId = showToast({ persistent: true, - icon: config.icon, - title: config.title, + icon, + title, description, actions: [ { - label: "Go to session", + label: language.t("notification.action.goToSession"), onClick: () => navigate(href), }, { - label: "Dismiss", + label: language.t("common.dismiss"), onClick: "dismiss", }, ], @@ -1024,7 +1017,7 @@ export default function Layout(props: ParentProps) { if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ - title: "Open project", + title: language.t("command.project.open"), multiple: true, }) resolve(result) @@ -1042,7 +1035,7 @@ export default function Layout(props: ParentProps) { if (data?.message) return data.message } if (err instanceof Error) return err.message - return "Request failed" + return language.t("common.requestFailed") } const deleteWorkspace = async (directory: string) => { @@ -1057,7 +1050,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to delete workspace", + title: language.t("workspace.delete.failed.title"), description: errorMessage(err), }) return false @@ -1079,9 +1072,15 @@ export default function Layout(props: ParentProps) { const current = currentProject() if (!current) return if (directory === current.worktree) return - setBusy(directory, true) + const progress = showToast({ + persistent: true, + title: language.t("workspace.resetting.title"), + description: language.t("workspace.resetting.description"), + }) + const dismiss = () => toaster.dismiss(progress) + const sessions = await globalSDK.client.session .list({ directory }) .then((x) => x.data ?? []) @@ -1092,7 +1091,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to reset workspace", + title: language.t("workspace.reset.failed.title"), description: errorMessage(err), }) return false @@ -1100,6 +1099,7 @@ export default function Layout(props: ParentProps) { if (!result) { setBusy(directory, false) + dismiss() return } @@ -1121,14 +1121,15 @@ export default function Layout(props: ParentProps) { await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) setBusy(directory, false) + dismiss() const href = `/${base64Encode(directory)}/session` navigate(href) layout.mobileSidebar.hide() showToast({ - title: "Workspace reset", - description: "Workspace now matches the default branch.", + title: language.t("workspace.reset.success.title"), + description: language.t("workspace.reset.success.description"), }) } @@ -1164,25 +1165,27 @@ export default function Layout(props: ParentProps) { } const description = () => { - if (data.status === "loading") return "Checking for unmerged changes..." - if (data.status === "error") return "Unable to verify git status." - if (!data.dirty) return "No unmerged changes detected." - return "Unmerged changes detected in this workspace." + if (data.status === "loading") return language.t("workspace.status.checking") + if (data.status === "error") return language.t("workspace.status.error") + if (!data.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") } return ( - <Dialog title="Delete workspace" fit> + <Dialog title={language.t("workspace.delete.title")} fit> <div class="flex flex-col gap-4 px-2.5 pb-3"> <div class="flex flex-col gap-1"> - <span class="text-14-regular text-text-strong">Delete workspace "{name()}"?</span> + <span class="text-14-regular text-text-strong"> + {language.t("workspace.delete.confirm", { name: name() })} + </span> <span class="text-12-regular text-text-weak">{description()}</span> </div> <div class="flex justify-end gap-2"> <Button variant="ghost" size="large" onClick={() => dialog.close()}> - Cancel + {language.t("common.cancel")} </Button> <Button variant="primary" size="large" disabled={data.status === "loading"} onClick={handleDelete}> - Delete workspace + {language.t("workspace.delete.button")} </Button> </div> </div> @@ -1235,34 +1238,36 @@ export default function Layout(props: ParentProps) { const archivedCount = () => state.sessions.length const description = () => { - if (state.status === "loading") return "Checking for unmerged changes..." - if (state.status === "error") return "Unable to verify git status." - if (!state.dirty) return "No unmerged changes detected." - return "Unmerged changes detected in this workspace." + if (state.status === "loading") return language.t("workspace.status.checking") + if (state.status === "error") return language.t("workspace.status.error") + if (!state.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") } const archivedLabel = () => { const count = archivedCount() - if (count === 0) return "No active sessions will be archived." - const label = count === 1 ? "1 session" : `${count} sessions` - return `${label} will be archived.` + if (count === 0) return language.t("workspace.reset.archived.none") + if (count === 1) return language.t("workspace.reset.archived.one") + return language.t("workspace.reset.archived.many", { count }) } return ( - <Dialog title="Reset workspace" fit> + <Dialog title={language.t("workspace.reset.title")} fit> <div class="flex flex-col gap-4 px-2.5 pb-3"> <div class="flex flex-col gap-1"> - <span class="text-14-regular text-text-strong">Reset workspace "{name()}"?</span> + <span class="text-14-regular text-text-strong"> + {language.t("workspace.reset.confirm", { name: name() })} + </span> <span class="text-12-regular text-text-weak"> - {description()} {archivedLabel()} This will reset the workspace to match the default branch. + {description()} {archivedLabel()} {language.t("workspace.reset.note")} </span> </div> <div class="flex justify-end gap-2"> <Button variant="ghost" size="large" onClick={() => dialog.close()}> - Cancel + {language.t("common.cancel")} </Button> <Button variant="primary" size="large" disabled={state.status === "loading"} onClick={handleReset}> - Reset workspace + {language.t("workspace.reset.button")} </Button> </div> </div> @@ -1536,7 +1541,10 @@ export default function Layout(props: ParentProps) { } > <HoverCard openDelay={1000} closeDelay={0} placement="right-start" gutter={28} trigger={item}> - <Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages…</div>}> + <Show + when={hoverReady()} + fallback={<div class="text-12-regular text-text-weak">{language.t("session.messages.loading")}</div>} + > <MessageNav messages={hoverMessages() ?? []} current={undefined} @@ -1561,7 +1569,7 @@ export default function Layout(props: ParentProps) { > <TooltipKeybind placement={props.mobile ? "bottom" : "right"} - title="Archive session" + title={language.t("command.session.archive")} keybind={command.keybind("session.archive")} gutter={8} > @@ -1604,7 +1612,8 @@ export default function Layout(props: ParentProps) { if (!directory) return const [workspaceStore] = globalSync.child(directory) - const kind = directory === project.worktree ? "local" : "sandbox" + const kind = + directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) return `${kind} : ${name}` }) @@ -1671,7 +1680,9 @@ export default function Layout(props: ParentProps) { <Spinner class="size-[15px]" /> </Show> </div> - <span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span> + <span class="text-14-medium text-text-base shrink-0"> + {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} : + </span> <Show when={!local()} fallback={ @@ -1737,7 +1748,7 @@ export default function Layout(props: ParentProps) { }} > <DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}> - <Tooltip value="More options" placement="top"> + <Tooltip value={language.t("common.moreOptions")} placement="top"> <DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" class="size-6 rounded-md" /> </Tooltip> <DropdownMenu.Portal> @@ -1750,7 +1761,7 @@ export default function Layout(props: ParentProps) { }} > <DropdownMenu.Item onSelect={() => navigate(`/${slug()}/session`)}> - <DropdownMenu.ItemLabel>New session</DropdownMenu.ItemLabel> + <DropdownMenu.ItemLabel>{language.t("command.session.new")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> <DropdownMenu.Item disabled={local()} @@ -1759,24 +1770,28 @@ export default function Layout(props: ParentProps) { setMenuOpen(false) }} > - <DropdownMenu.ItemLabel>Rename</DropdownMenu.ItemLabel> + <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> <DropdownMenu.Item disabled={local() || busy()} onSelect={() => dialog.show(() => <DialogResetWorkspace directory={props.directory} />)} > - <DropdownMenu.ItemLabel>Reset</DropdownMenu.ItemLabel> + <DropdownMenu.ItemLabel>{language.t("common.reset")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> <DropdownMenu.Item disabled={local() || busy()} onSelect={() => dialog.show(() => <DialogDeleteWorkspace directory={props.directory} />)} > - <DropdownMenu.ItemLabel>Delete</DropdownMenu.ItemLabel> + <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Portal> </DropdownMenu> - <TooltipKeybind placement="right" title="New session" keybind={command.keybind("session.new")}> + <TooltipKeybind + placement="right" + title={language.t("command.session.new")} + keybind={command.keybind("session.new")} + > <IconButton icon="plus-small" variant="ghost" @@ -1799,7 +1814,7 @@ export default function Layout(props: ParentProps) { icon="edit" class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3" > - New session + {language.t("command.session.new")} </Button> <Show when={loading()}> <SessionSkeleton /> @@ -1818,7 +1833,7 @@ export default function Layout(props: ParentProps) { ;(e.currentTarget as HTMLButtonElement).blur() }} > - Load more + {language.t("common.loadMore")} </Button> </div> </Show> @@ -1842,7 +1857,8 @@ export default function Layout(props: ParentProps) { const label = (directory: string) => { const [data] = globalSync.child(directory) - const kind = directory === props.project.worktree ? "local" : "sandbox" + const kind = + directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = workspaceLabel(directory, data.vcs?.branch, props.project.id) return `${kind} : ${name}` } @@ -1893,9 +1909,9 @@ export default function Layout(props: ParentProps) { trigger={trigger} onOpenChange={setOpen} > - <div class="-m-3 p-2 flex flex-col w-72"> - <div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div> - <div class="px-4 pb-2 text-12-medium text-text-weak">Recent sessions</div> + <div class="-m-3 p-2 flex flex-col w-72"> + <div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div> + <div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div> <div class="px-2 pb-2 flex flex-col gap-2"> <Show when={workspaceEnabled()} @@ -1951,7 +1967,7 @@ export default function Layout(props: ParentProps) { navigateToProject(props.project.worktree) }} > - View all sessions + {language.t("sidebar.project.viewAllSessions")} </Button> </div> </div> @@ -2002,7 +2018,7 @@ export default function Layout(props: ParentProps) { ;(e.currentTarget as HTMLButtonElement).blur() }} > - Load more + {language.t("common.loadMore")} </Button> </div> </Show> @@ -2033,7 +2049,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to create workspace", + title: language.t("workspace.create.failed.title"), description: errorMessage(err), }) return undefined @@ -2080,7 +2096,7 @@ export default function Layout(props: ParentProps) { placement={sidebarProps.mobile ? "bottom" : "right"} value={ <div class="flex items-center gap-2"> - <span>Open project</span> + <span>{language.t("command.project.open")}</span> <Show when={!sidebarProps.mobile}> <span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span> </Show> @@ -2098,12 +2114,12 @@ export default function Layout(props: ParentProps) { <div class="shrink-0 w-full pt-3 pb-3 flex flex-col items-center gap-2"> <TooltipKeybind placement={sidebarProps.mobile ? "bottom" : "right"} - title="Settings" + title={language.t("sidebar.settings")} keybind={command.keybind("settings.open")} > <IconButton icon="settings-gear" variant="ghost" size="large" onClick={openSettings} /> </TooltipKeybind> - <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help"> + <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value={language.t("sidebar.help")}> <IconButton icon="help" variant="ghost" @@ -2161,20 +2177,22 @@ export default function Layout(props: ParentProps) { class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" /> <DropdownMenu.Portal> - <DropdownMenu.Content class="mt-1"> - <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}> - <DropdownMenu.ItemLabel>Edit</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}> - <DropdownMenu.ItemLabel> - {layout.sidebar.workspaces(p.worktree)() ? "Disable workspaces" : "Enable workspaces"} - </DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Separator /> - <DropdownMenu.Item onSelect={() => closeProject(p.worktree)}> - <DropdownMenu.ItemLabel>Close</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - </DropdownMenu.Content> + <DropdownMenu.Content class="mt-1"> + <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}> + <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}> + <DropdownMenu.ItemLabel> + {layout.sidebar.workspaces(p.worktree)() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + </DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Separator /> + <DropdownMenu.Item onSelect={() => closeProject(p.worktree)}> + <DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + </DropdownMenu.Content> </DropdownMenu.Portal> </DropdownMenu> </div> @@ -2185,7 +2203,11 @@ export default function Layout(props: ParentProps) { fallback={ <> <div class="py-4 px-3"> - <TooltipKeybind title="New session" keybind={command.keybind("session.new")} placement="top"> + <TooltipKeybind + title={language.t("command.session.new")} + keybind={command.keybind("session.new")} + placement="top" + > <Button size="large" icon="plus-small" @@ -2195,7 +2217,7 @@ export default function Layout(props: ParentProps) { layout.mobileSidebar.hide() }} > - New session + {language.t("command.session.new")} </Button> </TooltipKeybind> </div> @@ -2208,12 +2230,12 @@ export default function Layout(props: ParentProps) { <> <div class="py-4 px-3"> <TooltipKeybind - title="New workspace" + title={language.t("workspace.new")} keybind={command.keybind("workspace.new")} placement="top" > <Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}> - New workspace + {language.t("workspace.new")} </Button> </TooltipKeybind> </div> @@ -2255,9 +2277,9 @@ export default function Layout(props: ParentProps) { <div class="shrink-0 px-2 py-3 border-t border-border-weak-base"> <div class="rounded-md bg-background-base shadow-xs-border-base"> <div class="p-3 flex flex-col gap-2"> - <div class="text-12-medium text-text-strong">Getting started</div> - <div class="text-text-base">OpenCode includes free models so you can start immediately.</div> - <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div> + <div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div> + <div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div> + <div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div> </div> <Button class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3" @@ -2265,7 +2287,7 @@ export default function Layout(props: ParentProps) { icon="plus" onClick={connectProvider} > - Connect provider + {language.t("command.provider.connect")} </Button> </div> </div> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 8803b063e..f98b02e7e 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1205,7 +1205,7 @@ export default function Page() { classes={{ button: "w-full" }} onClick={() => setStore("mobileTab", "session")} > - Session + {language.t("session.tab.session")} </Tabs.Trigger> <Tabs.Trigger value="review" @@ -1214,8 +1214,10 @@ export default function Page() { onClick={() => setStore("mobileTab", "review")} > <Switch> - <Match when={hasReview()}>{reviewCount()} Files Changed</Match> - <Match when={true}>Review</Match> + <Match when={hasReview()}> + {language.t("session.review.filesChanged", { count: reviewCount() })} + </Match> + <Match when={true}>{language.t("session.tab.review")}</Match> </Switch> </Tabs.Trigger> </Tabs.List> @@ -1245,7 +1247,9 @@ export default function Page() { <Match when={hasReview()}> <Show when={diffsReady()} - fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>} + fallback={ + <div class="px-4 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div> + } > <SessionReviewTab diffs={diffs} @@ -1265,13 +1269,13 @@ export default function Page() { </Show> </Match> <Match when={true}> - <div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6"> - <Mark class="w-14 opacity-10" /> - <div class="text-14-regular text-text-weak max-w-56">No changes in this session yet</div> - </div> - </Match> - </Switch> - </div> + <div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6"> + <Mark class="w-14 opacity-10" /> + <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div> + </div> + </Match> + </Switch> + </div> } > <div class="relative w-full h-full min-w-0"> @@ -1334,7 +1338,7 @@ export default function Page() { class="text-12-medium opacity-50" onClick={() => setStore("turnStart", 0)} > - Render earlier messages + {language.t("session.messages.renderEarlier")} </Button> </div> </Show> @@ -1352,7 +1356,9 @@ export default function Page() { sync.session.history.loadMore(id) }} > - {historyLoading() ? "Loading earlier messages..." : "Load earlier messages"} + {historyLoading() + ? language.t("session.messages.loadingEarlier") + : language.t("session.messages.loadEarlier")} </Button> </div> </Show> @@ -1436,7 +1442,7 @@ export default function Page() { when={prompt.ready()} fallback={ <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none"> - {handoff.prompt || "Loading prompt..."} + {handoff.prompt || language.t("prompt.loading")} </div> } > @@ -1482,11 +1488,11 @@ export default function Page() { <Show when={diffs()}> <DiffChanges changes={diffs()} variant="bars" /> </Show> - <div class="flex items-center gap-1.5"> - <div>Review</div> - <Show when={info()?.summary?.files}> - <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> - {info()?.summary?.files ?? 0} + <div class="flex items-center gap-1.5"> + <div>{language.t("session.tab.review")}</div> + <Show when={info()?.summary?.files}> + <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> + {info()?.summary?.files ?? 0} </div> </Show> </div> @@ -1497,7 +1503,7 @@ export default function Page() { <Tabs.Trigger value="context" closeButton={ - <Tooltip value="Close tab" placement="bottom"> + <Tooltip value={language.t("common.closeTab")} placement="bottom"> <IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} /> </Tooltip> } @@ -1506,7 +1512,7 @@ export default function Page() { > <div class="flex items-center gap-2"> <SessionContextUsage variant="indicator" /> - <div>Context</div> + <div>{language.t("session.tab.context")}</div> </div> </Tabs.Trigger> </Show> @@ -1515,7 +1521,7 @@ export default function Page() { </SortableProvider> <div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3"> <TooltipKeybind - title="Open file" + title={language.t("command.file.open")} keybind={command.keybind("file.open")} class="flex items-center" > @@ -1537,7 +1543,9 @@ export default function Page() { <Match when={hasReview()}> <Show when={diffsReady()} - fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>} + fallback={ + <div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div> + } > <SessionReviewTab diffs={diffs} @@ -1553,13 +1561,13 @@ export default function Page() { </Show> </Match> <Match when={true}> - <div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6"> - <Mark class="w-14 opacity-10" /> - <div class="text-14-regular text-text-weak max-w-56">No changes in this session yet</div> - </div> - </Match> - </Switch> - </div> + <div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6"> + <Mark class="w-14 opacity-10" /> + <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div> + </div> + </Match> + </Switch> + </div> </Show> </Tabs.Content> </Show> @@ -1735,7 +1743,9 @@ export default function Page() { }} > <Icon name="plus-small" size="small" /> - <span>Add {selectionLabel()} to context</span> + <span> + {language.t("session.context.addToContext", { selection: selectionLabel() ?? "" })} + </span> </button> </div> )} @@ -1792,7 +1802,7 @@ export default function Page() { /> </Match> <Match when={state()?.loading}> - <div class="px-6 py-4 text-text-weak">Loading...</div> + <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div> </Match> <Match when={state()?.error}> {(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>} @@ -1847,13 +1857,13 @@ export default function Page() { </div> )} </For> - <div class="flex-1" /> - <div class="text-text-weak pr-2">Loading...</div> - </div> - <div class="flex-1 flex items-center justify-center text-text-weak">Loading terminal...</div> + <div class="flex-1" /> + <div class="text-text-weak pr-2">{language.t("common.loading")}...</div> </div> - } - > + <div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div> + </div> + } + > <DragDropProvider onDragStart={handleTerminalDragStart} onDragEnd={handleTerminalDragEnd} @@ -1869,7 +1879,7 @@ export default function Page() { </SortableProvider> <div class="h-full flex items-center justify-center"> <TooltipKeybind - title="New terminal" + title={language.t("command.terminal.new")} keybind={command.keybind("terminal.new")} class="flex items-center" > |
