diff --git a/packages/app/package.json b/packages/app/package.json index c38068ed727c..e632efa5f345 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -37,6 +37,7 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 13d9d147e25a..f017815e0fad 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -21,6 +21,7 @@ import { NotificationProvider } from "@/context/notification" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { CommandProvider } from "@/context/command" import { Logo } from "@opencode-ai/ui/logo" +import { I18nProvider } from "@/i18n" import Layout from "@/pages/layout" import DirectoryLayout from "@/pages/directory-layout" import { ErrorPage } from "./pages/error" @@ -53,17 +54,19 @@ export function AppBaseProviders(props: ParentProps) { return ( - - }> - - - - {props.children} - - - - - + + + }> + + + + {props.children} + + + + + + ) } diff --git a/packages/app/src/components/dialog-select-language.tsx b/packages/app/src/components/dialog-select-language.tsx new file mode 100644 index 000000000000..5a057e601a74 --- /dev/null +++ b/packages/app/src/components/dialog-select-language.tsx @@ -0,0 +1,35 @@ +import { useI18n, supportedLocales } from "@/i18n" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" + +const languages = { + en: { name: "English", flag: "🇺🇸" }, + "zh-CN": { name: "简体中文", flag: "🇨🇳" }, + ja: { name: "日本語", flag: "🇯🇵" }, + fr: { name: "Français", flag: "🇫🇷" }, + es: { name: "Español", flag: "🇪🇸" }, +} as const + +export function DialogSelectLanguage() { + const { locale, setLocale } = useI18n() + + return ( + + lang} + current={supportedLocales.find((l) => l === locale())} + onSelect={(lang) => lang && setLocale(lang)} + > + {(lang) => ( +
+ {languages[lang].flag} + {languages[lang].name} +
+ )} +
+
+ ) +} diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4958ad2c353b..a38170bbd394 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -6,6 +6,7 @@ import { useServer } from "@/context/server" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useSync } from "@/context/sync" import { useGlobalSDK } from "@/context/global-sdk" +import { useI18n } from "@/i18n" import { getFilename } from "@opencode-ai/util/path" import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { iife } from "@opencode-ai/util/iife" @@ -31,6 +32,7 @@ export function SessionHeader() { const server = useServer() const dialog = useDialog() const sync = useSync() + const { t } = useI18n() const projectDirectory = createMemo(() => base64Decode(params.dir ?? "")) @@ -93,7 +95,7 @@ export function SessionHeader() { x.title} value={(x) => x.id} onSelect={(session) => { @@ -122,7 +124,7 @@ export function SessionHeader() { />
/
- +
- @@ -220,9 +222,9 @@ export function SessionHeader() { + } diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index 68ef0cc1f2b7..1f3536e565ef 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -4,6 +4,7 @@ import { useSync } from "@/context/sync" import { Icon } from "@opencode-ai/ui/icon" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Select } from "@opencode-ai/ui/select" +import { useI18n } from "@/i18n" const MAIN_WORKTREE = "main" const CREATE_WORKTREE = "create" @@ -15,6 +16,7 @@ interface NewSessionViewProps { export function NewSessionView(props: NewSessionViewProps) { const sync = useSync() + const { t } = useI18n() const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) @@ -32,13 +34,13 @@ export function NewSessionView(props: NewSessionViewProps) { const label = (value: string) => { if (value === MAIN_WORKTREE) { - if (isWorktree()) return "Main branch" + if (isWorktree()) return t().session.mainBranch const branch = sync.data.vcs?.branch - if (branch) return `Main branch (${branch})` - return "Main branch" + if (branch) return t().session.mainBranchWithName.replace("{branch}", branch) + return t().session.mainBranch } - if (value === CREATE_WORKTREE) return "Create new worktree" + if (value === CREATE_WORKTREE) return t().session.createWorktree return getFilename(value) } @@ -48,7 +50,7 @@ export function NewSessionView(props: NewSessionViewProps) { class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6" style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }} > -
New session
+
{t().session.newSession}
@@ -76,7 +78,7 @@ export function NewSessionView(props: NewSessionViewProps) {
- Last modified  + {t().session.lastModified}  {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()} diff --git a/packages/app/src/i18n/context.tsx b/packages/app/src/i18n/context.tsx new file mode 100644 index 000000000000..4835e3429eb6 --- /dev/null +++ b/packages/app/src/i18n/context.tsx @@ -0,0 +1,71 @@ +import { createContext, useContext, Accessor, Setter, createSignal, createRoot } from "solid-js" +import { useStorage } from "@solid-primitives/storage" +import { locales, type Locale, type Translation, defaultLocale, supportedLocales } from "./locales" + +type I18nContextValue = { + locale: Accessor + setLocale: Setter + t: Accessor + locales: typeof supportedLocales +} + +const I18nContext = createContext() + +export function I18nProvider(props: { children: any }) { + const [locale, setLocale] = createSignal(defaultLocale) + + // Initialize from localStorage or browser language + const initializeLocale = () => { + const stored = localStorage.getItem("opencode-locale") as Locale | null + if (stored && supportedLocales.includes(stored)) { + setLocale(stored) + return + } + + // Detect browser language + const browserLang = navigator.language + if (browserLang.startsWith("zh")) { + setLocale("zh-CN") + } else if (browserLang.startsWith("ja")) { + setLocale("ja") + } else if (browserLang.startsWith("fr")) { + setLocale("fr") + } else if (browserLang.startsWith("es")) { + setLocale("es") + } + } + initializeLocale() + + // Persist locale changes + const persistLocale: Setter = (...args) => { + const result = setLocale(...args) + const newLocale = locale() + localStorage.setItem("opencode-locale", newLocale) + return result + } + + const t = () => locales[locale()] + + const value: I18nContextValue = { + locale, + setLocale: persistLocale, + t, + locales: supportedLocales, + } + + return {props.children} +} + +export function useI18n() { + const context = useContext(I18nContext) + if (!context) { + throw new Error("useI18n must be used within I18nProvider") + } + return context +} + +// Helper to get nested translation values +export function useT() { + const { t } = useI18n() + return t +} diff --git a/packages/app/src/i18n/index.ts b/packages/app/src/i18n/index.ts new file mode 100644 index 000000000000..580f87100eb4 --- /dev/null +++ b/packages/app/src/i18n/index.ts @@ -0,0 +1,2 @@ +export { I18nProvider, useI18n, useT } from "./context" +export { locales, defaultLocale, supportedLocales, type Locale, type Translation } from "./locales" diff --git a/packages/app/src/i18n/locales/en.ts b/packages/app/src/i18n/locales/en.ts new file mode 100644 index 000000000000..8557f8c39037 --- /dev/null +++ b/packages/app/src/i18n/locales/en.ts @@ -0,0 +1,108 @@ +export default { + common: { + loading: "Loading...", + save: "Save", + cancel: "Cancel", + confirm: "Confirm", + delete: "Delete", + edit: "Edit", + search: "Search", + back: "Back", + next: "Next", + close: "Close", + }, + home: { + title: "OpenCode AI", + subtitle: "AI-powered development tool", + start: "Start Coding", + recentProjects: "Recent projects", + noRecentProjects: "No recent projects", + getStarted: "Get started by opening a local project", + openProject: "Open project", + }, + session: { + new: "New Session", + newSession: "New session", + mainBranch: "Main branch", + mainBranchWithName: "Main branch ({branch})", + createWorktree: "Create new worktree", + lastModified: "Last modified", + backToParent: "Back to parent session", + share: "Share session", + terminate: "Terminate", + archive: "Archive session", + filesChanged: "{count} file{plural} changed", + }, + dialog: { + selectProvider: { + title: "Select Provider", + description: "Choose an AI provider to use", + }, + selectModel: { + title: "Select Model", + description: "Choose a model to use", + unpaid: { + title: "Model Payment Required", + description: "This model requires payment to use", + }, + }, + selectServer: { + title: "Select Server", + description: "Choose a server to connect to", + }, + selectDirectory: { + title: "Select Directory", + description: "Choose a directory to work with", + openProject: "Open project", + }, + selectFile: { + title: "Select File", + description: "Choose a file to work with", + }, + selectMcp: { + title: "Select MCP", + description: "Choose a Model Context Protocol server", + }, + connectProvider: { + title: "Connect Provider", + description: "Configure your AI provider credentials", + }, + editProject: { + title: "Edit Project", + description: "Edit project settings", + editProject: "Edit project", + closeProject: "Close project", + }, + manageModels: { + title: "Manage Models", + description: "Manage available models", + }, + }, + terminal: { + tabs: { + session: "Session", + context: "Context", + lsp: "LSP", + mcp: "MCP", + }, + }, + fileTree: { + empty: "No files found", + refresh: "Refresh", + }, + sidebar: { + toggle: "Toggle sidebar", + newSession: "New session", + loadMore: "Load more", + gettingStarted: "Getting started", + gettingStartedDesc1: "OpenCode includes free models so you can start immediately.", + gettingStartedDesc2: "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", + connectProvider: "Connect provider", + shareFeedback: "Share feedback", + changeLanguage: "Change language", + }, + layout: { + editProject: "Edit project", + closeProject: "Close project", + }, +} as const diff --git a/packages/app/src/i18n/locales/es.ts b/packages/app/src/i18n/locales/es.ts new file mode 100644 index 000000000000..207da6fd659d --- /dev/null +++ b/packages/app/src/i18n/locales/es.ts @@ -0,0 +1,108 @@ +export default { + common: { + loading: "Cargando...", + save: "Guardar", + cancel: "Cancelar", + confirm: "Confirmar", + delete: "Eliminar", + edit: "Editar", + search: "Buscar", + back: "Atrás", + next: "Siguiente", + close: "Cerrar", + }, + home: { + title: "OpenCode AI", + subtitle: "Herramienta de desarrollo impulsada por IA", + start: "Comenzar a codificar", + recentProjects: "Proyectos recientes", + noRecentProjects: "Sin proyectos recientes", + getStarted: "Abre un proyecto local para comenzar", + openProject: "Abrir proyecto", + }, + session: { + new: "Nueva sesión", + newSession: "Nueva sesión", + mainBranch: "Rama principal", + mainBranchWithName: "Rama principal ({branch})", + createWorktree: "Crear nuevo árbol de trabajo", + lastModified: "Última modificación", + backToParent: "Volver a la sesión principal", + share: "Compartir sesión", + terminate: "Terminar", + archive: "Archivar sesión", + filesChanged: "{count} archivo{plural} modificado{plural}", + }, + dialog: { + selectProvider: { + title: "Seleccionar proveedor", + description: "Elige un proveedor de IA para usar", + }, + selectModel: { + title: "Seleccionar modelo", + description: "Elige un modelo para usar", + unpaid: { + title: "Pago del modelo requerido", + description: "Este modelo requiere pago para usarse", + }, + }, + selectServer: { + title: "Seleccionar servidor", + description: "Elige un servidor al cual conectarse", + }, + selectDirectory: { + title: "Seleccionar directorio", + description: "Elige un directorio con el cual trabajar", + openProject: "Abrir proyecto", + }, + selectFile: { + title: "Seleccionar archivo", + description: "Elige un archivo con el cual trabajar", + }, + selectMcp: { + title: "Seleccionar MCP", + description: "Elige un servidor Model Context Protocol", + }, + connectProvider: { + title: "Conectar proveedor", + description: "Configura tus credenciales de proveedor de IA", + }, + editProject: { + title: "Editar proyecto", + description: "Edita la configuración del proyecto", + editProject: "Editar proyecto", + closeProject: "Cerrar proyecto", + }, + manageModels: { + title: "Administrar modelos", + description: "Administra los modelos disponibles", + }, + }, + terminal: { + tabs: { + session: "Sesión", + context: "Contexto", + lsp: "LSP", + mcp: "MCP", + }, + }, + fileTree: { + empty: "No se encontraron archivos", + refresh: "Actualizar", + }, + sidebar: { + toggle: "Alternar barra lateral", + newSession: "Nueva sesión", + loadMore: "Cargar más", + gettingStarted: "Comenzando", + gettingStartedDesc1: "OpenCode incluye modelos gratuitos para que puedas comenzar de inmediato.", + gettingStartedDesc2: "Conecta cualquier proveedor para usar modelos, incl. Claude, GPT, Gemini, etc.", + connectProvider: "Conectar proveedor", + shareFeedback: "Comentar comentarios", + changeLanguage: "Cambiar idioma", + }, + layout: { + editProject: "Editar proyecto", + closeProject: "Cerrar proyecto", + }, +} as const diff --git a/packages/app/src/i18n/locales/fr.ts b/packages/app/src/i18n/locales/fr.ts new file mode 100644 index 000000000000..eb52d3e531a7 --- /dev/null +++ b/packages/app/src/i18n/locales/fr.ts @@ -0,0 +1,108 @@ +export default { + common: { + loading: "Chargement...", + save: "Enregistrer", + cancel: "Annuler", + confirm: "Confirmer", + delete: "Supprimer", + edit: "Modifier", + search: "Rechercher", + back: "Retour", + next: "Suivant", + close: "Fermer", + }, + home: { + title: "OpenCode AI", + subtitle: "Outil de développement alimenté par l'IA", + start: "Commencer à coder", + recentProjects: "Projets récents", + noRecentProjects: "Aucun projet récent", + getStarted: "Ouvrez un projet local pour commencer", + openProject: "Ouvrir un projet", + }, + session: { + new: "Nouvelle session", + newSession: "Nouvelle session", + mainBranch: "Branche principale", + mainBranchWithName: "Branche principale ({branch})", + createWorktree: "Créer un nouvel arbre de travail", + lastModified: "Dernière modification", + backToParent: "Retour à la session parente", + share: "Partager la session", + terminate: "Terminer", + archive: "Archiver la session", + filesChanged: "{count} fichier{plural} modifié{plural}", + }, + dialog: { + selectProvider: { + title: "Sélectionner un fournisseur", + description: "Choisir un fournisseur d'IA à utiliser", + }, + selectModel: { + title: "Sélectionner un modèle", + description: "Choisir un modèle à utiliser", + unpaid: { + title: "Paiement du modèle requis", + description: "Ce modèle nécessite un paiement pour être utilisé", + }, + }, + selectServer: { + title: "Sélectionner un serveur", + description: "Choisir un serveur auquel se connecter", + }, + selectDirectory: { + title: "Sélectionner un répertoire", + description: "Choisir un répertoire avec lequel travailler", + openProject: "Ouvrir un projet", + }, + selectFile: { + title: "Sélectionner un fichier", + description: "Choisir un fichier avec lequel travailler", + }, + selectMcp: { + title: "Sélectionner un MCP", + description: "Choisir un serveur Model Context Protocol", + }, + connectProvider: { + title: "Connecter un fournisseur", + description: "Configurer vos identifiants de fournisseur d'IA", + }, + editProject: { + title: "Modifier le projet", + description: "Modifier les paramètres du projet", + editProject: "Modifier le projet", + closeProject: "Fermer le projet", + }, + manageModels: { + title: "Gérer les modèles", + description: "Gérer les modèles disponibles", + }, + }, + terminal: { + tabs: { + session: "Session", + context: "Contexte", + lsp: "LSP", + mcp: "MCP", + }, + }, + fileTree: { + empty: "Aucun fichier trouvé", + refresh: "Actualiser", + }, + sidebar: { + toggle: "Afficher/Masquer la barre latérale", + newSession: "Nouvelle session", + loadMore: "Charger plus", + gettingStarted: "Premiers pas", + gettingStartedDesc1: "OpenCode inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", + gettingStartedDesc2: "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini, etc.", + connectProvider: "Connecter un fournisseur", + shareFeedback: "Partager des commentaires", + changeLanguage: "Changer de langue", + }, + layout: { + editProject: "Modifier le projet", + closeProject: "Fermer le projet", + }, +} as const diff --git a/packages/app/src/i18n/locales/index.ts b/packages/app/src/i18n/locales/index.ts new file mode 100644 index 000000000000..ddf15d5609c0 --- /dev/null +++ b/packages/app/src/i18n/locales/index.ts @@ -0,0 +1,19 @@ +import en from "./en" +import zhCN from "./zh-CN" +import ja from "./ja" +import fr from "./fr" +import es from "./es" + +export const locales = { + en, + "zh-CN": zhCN, + ja, + fr, + es, +} as const + +export type Locale = keyof typeof locales +export const defaultLocale: Locale = "en" +export const supportedLocales: Locale[] = Object.keys(locales) as Locale[] + +export type Translation = typeof locales[Locale] diff --git a/packages/app/src/i18n/locales/ja.ts b/packages/app/src/i18n/locales/ja.ts new file mode 100644 index 000000000000..cf59d9878ae4 --- /dev/null +++ b/packages/app/src/i18n/locales/ja.ts @@ -0,0 +1,108 @@ +export default { + common: { + loading: "読み込み中...", + save: "保存", + cancel: "キャンセル", + confirm: "確認", + delete: "削除", + edit: "編集", + search: "検索", + back: "戻る", + next: "次へ", + close: "閉じる", + }, + home: { + title: "OpenCode AI", + subtitle: "AI による開発ツール", + start: "コーディングを開始", + recentProjects: "最近のプロジェクト", + noRecentProjects: "最近のプロジェクトがありません", + getStarted: "ローカルプロジェクトを開いて始めましょう", + openProject: "プロジェクトを開く", + }, + session: { + new: "新しいセッション", + newSession: "新しいセッション", + mainBranch: "メインブランチ", + mainBranchWithName: "メインブランチ ({branch})", + createWorktree: "新しいワークツリーを作成", + lastModified: "最終更新", + backToParent: "親セッションに戻る", + share: "セッションを共有", + terminate: "終了", + archive: "セッションをアーカイブ", + filesChanged: "{count} ファイルが変更されました", + }, + dialog: { + selectProvider: { + title: "プロバイダーを選択", + description: "使用する AI プロバイダーを選択してください", + }, + selectModel: { + title: "モデルを選択", + description: "使用するモデルを選択してください", + unpaid: { + title: "モデルの支払いが必要", + description: "このモデルを使用するには支払いが必要です", + }, + }, + selectServer: { + title: "サーバーを選択", + description: "接続するサーバーを選択してください", + }, + selectDirectory: { + title: "ディレクトリを選択", + description: "使用するディレクトリを選択してください", + openProject: "プロジェクトを開く", + }, + selectFile: { + title: "ファイルを選択", + description: "使用するファイルを選択してください", + }, + selectMcp: { + title: "MCP を選択", + description: "モデル コンテキスト プロトコル サーバーを選択してください", + }, + connectProvider: { + title: "プロバイダーに接続", + description: "AI プロバイダーの認証情報を設定してください", + }, + editProject: { + title: "プロジェクトを編集", + description: "プロジェクト設定を編集してください", + editProject: "プロジェクトを編集", + closeProject: "プロジェクトを閉じる", + }, + manageModels: { + title: "モデルを管理", + description: "利用可能なモデルを管理してください", + }, + }, + terminal: { + tabs: { + session: "セッション", + context: "コンテキスト", + lsp: "LSP", + mcp: "MCP", + }, + }, + fileTree: { + empty: "ファイルが見つかりません", + refresh: "更新", + }, + sidebar: { + toggle: "サイドバーを切り替え", + newSession: "新しいセッション", + loadMore: "もっと見る", + gettingStarted: "はじめに", + gettingStartedDesc1: "OpenCode には無料モデルが含まれており、すぐに始められます。", + gettingStartedDesc2: "プロバイダーを接続してモデルを使用できます(Claude、GPT、Gemini など)。", + connectProvider: "プロバイダーに接続", + shareFeedback: "フィードバックを共有", + changeLanguage: "言語を変更", + }, + layout: { + editProject: "プロジェクトを編集", + closeProject: "プロジェクトを閉じる", + }, +} as const diff --git a/packages/app/src/i18n/locales/zh-CN.ts b/packages/app/src/i18n/locales/zh-CN.ts new file mode 100644 index 000000000000..ec32202d07f7 --- /dev/null +++ b/packages/app/src/i18n/locales/zh-CN.ts @@ -0,0 +1,108 @@ +export default { + common: { + loading: "加载中...", + save: "保存", + cancel: "取消", + confirm: "确认", + delete: "删除", + edit: "编辑", + search: "搜索", + back: "返回", + next: "下一步", + close: "关闭", + }, + home: { + title: "OpenCode AI", + subtitle: "AI 驱动的开发工具", + start: "开始编码", + recentProjects: "最近的项目", + noRecentProjects: "没有最近的项目", + getStarted: "打开本地项目开始使用", + openProject: "打开项目", + }, + session: { + new: "新会话", + newSession: "新会话", + mainBranch: "主分支", + mainBranchWithName: "主分支 ({branch})", + createWorktree: "创建新工作树", + lastModified: "最后修改", + backToParent: "返回父会话", + share: "分享会话", + terminate: "终止", + archive: "归档会话", + filesChanged: "{count} 个文件已更改", + }, + dialog: { + selectProvider: { + title: "选择提供商", + description: "选择要使用的 AI 提供商", + }, + selectModel: { + title: "选择模型", + description: "选择要使用的模型", + unpaid: { + title: "模型需要付费", + description: "此模型需要付费才能使用", + }, + }, + selectServer: { + title: "选择服务器", + description: "选择要连接的服务器", + }, + selectDirectory: { + title: "选择目录", + description: "选择要使用的目录", + openProject: "打开项目", + }, + selectFile: { + title: "选择文件", + description: "选择要使用的文件", + }, + selectMcp: { + title: "选择 MCP", + description: "选择模型上下文协议服务器", + }, + connectProvider: { + title: "连接提供商", + description: "配置您的 AI 提供商凭据", + }, + editProject: { + title: "编辑项目", + description: "编辑项目设置", + editProject: "编辑项目", + closeProject: "关闭项目", + }, + manageModels: { + title: "管理模型", + description: "管理可用模型", + }, + }, + terminal: { + tabs: { + session: "会话", + context: "上下文", + lsp: "LSP", + mcp: "MCP", + }, + }, + fileTree: { + empty: "未找到文件", + refresh: "刷新", + }, + sidebar: { + toggle: "切换侧边栏", + newSession: "新会话", + loadMore: "加载更多", + gettingStarted: "入门指南", + gettingStartedDesc1: "OpenCode 包含免费模型,您可以立即开始使用。", + gettingStartedDesc2: "连接任何提供商以使用模型,包括 Claude、GPT、Gemini 等。", + connectProvider: "连接提供商", + shareFeedback: "分享反馈", + changeLanguage: "切换语言", + }, + layout: { + editProject: "编辑项目", + closeProject: "关闭项目", + }, +} as const diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 275113566ad1..8e3358d0238b 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -12,6 +12,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" import { useServer } from "@/context/server" +import { useI18n } from "@/i18n" 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 { t } = useI18n() const homedir = createMemo(() => sync.data.path.home) function openProject(directory: string) { @@ -40,7 +42,7 @@ export default function Home() { if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ - title: "Open project", + title: t().home.openProject, multiple: true, }) resolve(result) @@ -75,9 +77,9 @@ export default function Home() { 0}>
-
Recent projects
+
{t().home.recentProjects}
    @@ -107,12 +109,12 @@ export default function Home() {
    -
    No recent projects
    -
    Get started by opening a local project
    +
    {t().home.noRecentProjects}
    +
    {t().home.getStarted}
    diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 85d61d57beb9..c7dd5517d023 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -53,10 +53,12 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogEditProject } from "@/components/dialog-edit-project" import { DialogSelectServer } from "@/components/dialog-select-server" +import { DialogSelectLanguage } from "@/components/dialog-select-language" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { useServer } from "@/context/server" +import { useI18n } from "@/i18n" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -91,6 +93,7 @@ export default function Layout(props: ParentProps) { const dialog = useDialog() const command = useCommand() const theme = useTheme() + const { t } = useI18n() const availableThemeEntries = createMemo(() => Object.entries(theme.themes())) const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] const colorSchemeLabel: Record = { @@ -176,7 +179,7 @@ export default function Layout(props: ParentProps) { const session = store.session.find((s) => s.id === perm.sessionID) const sessionKey = `${directory}:${perm.sessionID}` - const sessionTitle = session?.title ?? "New session" + const sessionTitle = session?.title ?? t().session.newSession const projectName = getFilename(directory) const description = `${sessionTitle} in ${projectName} needs permission` const href = `/${base64Encode(directory)}/session/${perm.sessionID}` @@ -359,21 +362,21 @@ export default function Layout(props: ParentProps) { const commands: CommandOption[] = [ { id: "sidebar.toggle", - title: "Toggle sidebar", + title: t().sidebar.toggle, category: "View", keybind: "mod+b", onSelect: () => layout.sidebar.toggle(), }, { id: "project.open", - title: "Open project", + title: t().home.openProject, category: "Project", keybind: "mod+o", onSelect: () => chooseProject(), }, { id: "provider.connect", - title: "Connect provider", + title: t().sidebar.connectProvider, category: "Provider", onSelect: () => connectProvider(), }, @@ -397,9 +400,19 @@ export default function Layout(props: ParentProps) { keybind: "alt+arrowdown", onSelect: () => navigateSessionByOffset(1), }, + { + id: "session.new", + title: t().session.newSession, + category: "Session", + keybind: "mod+n", + onSelect: () => { + const current = params.dir ? base64Decode(params.dir) : undefined + if (current) navigate(`/${params.dir}/session`) + }, + }, { id: "session.archive", - title: "Archive session", + title: t().session.archive, category: "Session", keybind: "mod+shift+backspace", disabled: !params.dir || !params.id, @@ -732,7 +745,7 @@ export default function Layout(props: ParentProps) { @@ -842,14 +855,14 @@ export default function Layout(props: ParentProps) { >
    - +
    - New session + {t().sidebar.newSession}
    @@ -866,7 +879,7 @@ export default function Layout(props: ParentProps) { size="large" onClick={loadMoreSessions} > - Load more + {t().sidebar.loadMore}
    @@ -918,8 +931,7 @@ export default function Layout(props: ParentProps) { @@ -948,7 +960,7 @@ export default function Layout(props: ParentProps) {
    @@ -985,24 +997,24 @@ export default function Layout(props: ParentProps) { 0 && !providers.paid().length && expanded()}>
    -
    Getting started
    -
    OpenCode includes free models so you can start immediately.
    -
    Connect any provider to use models, inc. Claude, GPT, Gemini etc.
    +
    {t().sidebar.gettingStarted}
    +
    {t().sidebar.gettingStartedDesc1}
    +
    {t().sidebar.gettingStartedDesc2}
    - +
    0}> - + @@ -1019,7 +1031,7 @@ export default function Layout(props: ParentProps) { placement="right" value={
    - Open project + {t().home.openProject} {command.keybind("project.open")} @@ -1034,10 +1046,10 @@ export default function Layout(props: ParentProps) { icon="folder-add-left" onClick={chooseProject} > - Open project + {t().home.openProject} - + + + +