Adam 2 месяцев назад
Родитель
Сommit
a68e5a1c17

+ 77 - 0
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.",
 }

+ 77 - 0
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>>

+ 13 - 11
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>

+ 120 - 98
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>

+ 49 - 39
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"
                     >