diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index b57de0ae464..05def31ec9b 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -10,6 +10,7 @@ import matter from "gray-matter" import { Instance } from "../../project/instance" import { EOL } from "os" import type { Argv } from "yargs" +import { t } from "../../i18n" type AgentMode = "all" | "primary" | "subagent" @@ -67,7 +68,7 @@ const AgentCreateCommand = cmd({ if (!isFullyNonInteractive) { UI.empty() - prompts.intro("Create agent") + prompts.intro(t("agent.create_title")) } const project = Instance.project @@ -80,15 +81,15 @@ const AgentCreateCommand = cmd({ let scope: "global" | "project" = "global" if (project.vcs === "git") { const scopeResult = await prompts.select({ - message: "Location", + message: t("agent.location"), options: [ { - label: "Current project", + label: t("agent.current_project"), value: "project" as const, hint: Instance.worktree, }, { - label: "Global", + label: t("agent.global"), value: "global" as const, hint: Global.Path.config, }, @@ -109,9 +110,9 @@ const AgentCreateCommand = cmd({ description = cliDescription } else { const query = await prompts.text({ - message: "Description", - placeholder: "What should this agent do?", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + message: t("agent.description"), + placeholder: t("agent.description_placeholder"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(query)) throw new UI.CancelledError() description = query @@ -119,22 +120,22 @@ const AgentCreateCommand = cmd({ // Generate agent const spinner = prompts.spinner() - spinner.start("Generating agent configuration...") + spinner.start(t("agent.generating")) const model = args.model ? Provider.parseModel(args.model) : undefined const generated = await Agent.generate({ description, model }).catch((error) => { - spinner.stop(`LLM failed to generate agent: ${error.message}`, 1) + spinner.stop(t("agent.llm_failed", { error: error.message }), 1) if (isFullyNonInteractive) process.exit(1) throw new UI.CancelledError() }) - spinner.stop(`Agent ${generated.identifier} generated`) + spinner.stop(t("agent.generated", { name: generated.identifier })) // Select tools let selectedTools: string[] if (cliTools !== undefined) { - selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS + selectedTools = cliTools ? cliTools.split(",").map((x) => x.trim()) : AVAILABLE_TOOLS } else { const result = await prompts.multiselect({ - message: "Select tools to enable", + message: t("agent.select_tools"), options: AVAILABLE_TOOLS.map((tool) => ({ label: tool, value: tool, @@ -151,22 +152,22 @@ const AgentCreateCommand = cmd({ mode = cliMode } else { const modeResult = await prompts.select({ - message: "Agent mode", + message: t("agent.mode"), options: [ { - label: "All", + label: t("agent.mode_all"), value: "all" as const, - hint: "Can function in both primary and subagent roles", + hint: t("agent.mode_all_hint"), }, { - label: "Primary", + label: t("agent.mode_primary"), value: "primary" as const, - hint: "Acts as a primary/main agent", + hint: t("agent.mode_primary_hint"), }, { - label: "Subagent", + label: t("agent.mode_subagent"), value: "subagent" as const, - hint: "Can be used as a subagent by other agents", + hint: t("agent.mode_subagent_hint"), }, ], initialValue: "all" as const, @@ -205,10 +206,10 @@ const AgentCreateCommand = cmd({ const file = Bun.file(filePath) if (await file.exists()) { if (isFullyNonInteractive) { - console.error(`Error: Agent file already exists: ${filePath}`) + console.error(`Error: ${t("agent.file_exists", { path: filePath })}`) process.exit(1) } - prompts.log.error(`Agent file already exists: ${filePath}`) + prompts.log.error(t("agent.file_exists", { path: filePath })) throw new UI.CancelledError() } @@ -217,8 +218,8 @@ const AgentCreateCommand = cmd({ if (isFullyNonInteractive) { console.log(filePath) } else { - prompts.log.success(`Agent created: ${filePath}`) - prompts.outro("Done") + prompts.log.success(t("agent.created", { path: filePath })) + prompts.outro(t("upgrade.done")) } }, }) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index f200ec4fe06..7c40e4abd20 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -10,6 +10,7 @@ import { Config } from "../../config/config" import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" +import { t } from "../../i18n" import type { Hooks } from "@opencode-ai/plugin" type PluginAuth = NonNullable @@ -22,7 +23,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): let index = 0 if (plugin.auth.methods.length > 1) { const method = await prompts.select({ - message: "Login method", + message: t("auth.login_method"), options: [ ...plugin.auth.methods.map((x, index) => ({ label: x.label, @@ -66,7 +67,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): const authorize = await method.authorize(inputs) if (authorize.url) { - prompts.log.info("Go to: " + authorize.url) + prompts.log.info(t("auth.go_to", { url: authorize.url })) } if (authorize.method === "auto") { @@ -74,10 +75,10 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): prompts.log.info(authorize.instructions) } const spinner = prompts.spinner() - spinner.start("Waiting for authorization...") + spinner.start(t("auth.waiting_for_auth")) const result = await authorize.callback() if (result.type === "failed") { - spinner.stop("Failed to authorize", 1) + spinner.stop(t("auth.failed_to_authorize"), 1) } if (result.type === "success") { const saveProvider = result.provider ?? provider @@ -97,19 +98,19 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): key: result.key, }) } - spinner.stop("Login successful") + spinner.stop(t("auth.login_successful")) } } if (authorize.method === "code") { const code = await prompts.text({ - message: "Paste the authorization code here: ", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + message: t("auth.paste_code"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(code)) throw new UI.CancelledError() const result = await authorize.callback(code) if (result.type === "failed") { - prompts.log.error("Failed to authorize") + prompts.log.error(t("auth.failed_to_authorize")) } if (result.type === "success") { const saveProvider = result.provider ?? provider @@ -129,11 +130,11 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): key: result.key, }) } - prompts.log.success("Login successful") + prompts.log.success(t("auth.login_successful")) } } - prompts.outro("Done") + prompts.outro(t("upgrade.done")) return true } @@ -141,7 +142,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): if (method.authorize) { const result = await method.authorize(inputs) if (result.type === "failed") { - prompts.log.error("Failed to authorize") + prompts.log.error(t("auth.failed_to_authorize")) } if (result.type === "success") { const saveProvider = result.provider ?? provider @@ -149,9 +150,9 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): type: "api", key: result.key, }) - prompts.log.success("Login successful") + prompts.log.success(t("auth.login_successful")) } - prompts.outro("Done") + prompts.outro(t("upgrade.done")) return true } } @@ -176,7 +177,7 @@ export const AuthListCommand = cmd({ const authPath = path.join(Global.Path.data, "auth.json") const homedir = os.homedir() const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath - prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) + prompts.intro(`${t("auth.credentials")} ${UI.Style.TEXT_DIM}${displayPath}`) const results = Object.entries(await Auth.all()) const database = await ModelsDev.get() @@ -185,7 +186,7 @@ export const AuthListCommand = cmd({ prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`) } - prompts.outro(`${results.length} credentials`) + prompts.outro(t("auth.credentials_count", { count: String(results.length) })) // Environment variables section const activeEnvVars: Array<{ provider: string; envVar: string }> = [] @@ -203,13 +204,17 @@ export const AuthListCommand = cmd({ if (activeEnvVars.length > 0) { UI.empty() - prompts.intro("Environment") + prompts.intro(t("auth.environment")) for (const { provider, envVar } of activeEnvVars) { prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`) } - prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s")) + prompts.outro( + activeEnvVars.length === 1 + ? t("auth.env_var_count", { count: String(activeEnvVars.length) }) + : t("auth.env_vars_count", { count: String(activeEnvVars.length) }), + ) } }, }) @@ -227,18 +232,18 @@ export const AuthLoginCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("Add credential") + prompts.intro(t("auth.add_credential")) if (args.url) { const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any) - prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``) + prompts.log.info(t("auth.running_command", { command: wellknown.auth.command.join(" ") })) const proc = Bun.spawn({ cmd: wellknown.auth.command, stdout: "pipe", }) const exit = await proc.exited if (exit !== 0) { - prompts.log.error("Failed") - prompts.outro("Done") + prompts.log.error(t("auth.failed")) + prompts.outro(t("upgrade.done")) return } const token = await new Response(proc.stdout).text() @@ -247,8 +252,8 @@ export const AuthLoginCommand = cmd({ key: wellknown.auth.env, token: token.trim(), }) - prompts.log.success("Logged into " + args.url) - prompts.outro("Done") + prompts.log.success(t("auth.logged_into", { url: args.url })) + prompts.outro(t("upgrade.done")) return } await ModelsDev.refresh().catch(() => {}) @@ -278,7 +283,7 @@ export const AuthLoginCommand = cmd({ vercel: 6, } let provider = await prompts.autocomplete({ - message: "Select provider", + message: t("auth.select_provider"), maxItems: 8, options: [ ...pipe( @@ -292,14 +297,14 @@ export const AuthLoginCommand = cmd({ label: x.name, value: x.id, hint: { - opencode: "recommended", - anthropic: "Claude Max or API key", + opencode: t("auth.recommended"), + anthropic: t("auth.claude_max_or_api"), }[x.id], })), ), { value: "other", - label: "Other", + label: t("auth.other"), }, ], }) @@ -314,8 +319,8 @@ export const AuthLoginCommand = cmd({ if (provider === "other") { provider = await prompts.text({ - message: "Enter provider id", - validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"), + message: t("auth.enter_provider_id"), + validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : t("auth.provider_id_format")), }) if (prompts.isCancel(provider)) throw new UI.CancelledError() provider = provider.replace(/^@ai-sdk\//, "") @@ -328,36 +333,30 @@ export const AuthLoginCommand = cmd({ if (handled) return } - prompts.log.warn( - `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`, - ) + prompts.log.warn(t("auth.custom_provider_warning", { provider })) } if (provider === "amazon-bedrock") { - prompts.log.info( - "Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID", - ) - prompts.outro("Done") + prompts.log.info(t("auth.bedrock_info")) + prompts.outro(t("upgrade.done")) return } if (provider === "opencode") { - prompts.log.info("Create an api key at https://opencode.ai/auth") + prompts.log.info(t("auth.opencode_info")) } if (provider === "vercel") { - prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token") + prompts.log.info(t("auth.vercel_info")) } if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) { - prompts.log.info( - "Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway", - ) + prompts.log.info(t("auth.cloudflare_info")) } const key = await prompts.password({ - message: "Enter your API key", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + message: t("auth.enter_api_key"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(key)) throw new UI.CancelledError() await Auth.set(provider, { @@ -365,7 +364,7 @@ export const AuthLoginCommand = cmd({ key, }) - prompts.outro("Done") + prompts.outro(t("upgrade.done")) }, }) }, @@ -377,14 +376,14 @@ export const AuthLogoutCommand = cmd({ async handler() { UI.empty() const credentials = await Auth.all().then((x) => Object.entries(x)) - prompts.intro("Remove credential") + prompts.intro(t("auth.remove_credential")) if (credentials.length === 0) { - prompts.log.error("No credentials found") + prompts.log.error(t("auth.no_credentials")) return } const database = await ModelsDev.get() const providerID = await prompts.select({ - message: "Select provider", + message: t("auth.select_provider"), options: credentials.map(([key, value]) => ({ label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", value: key, @@ -392,6 +391,6 @@ export const AuthLogoutCommand = cmd({ }) if (prompts.isCancel(providerID)) throw new UI.CancelledError() await Auth.remove(providerID) - prompts.outro("Logout successful") + prompts.outro(t("auth.logout_successful")) }, }) diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 27f460501ab..71df36f815e 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -5,6 +5,7 @@ import { bootstrap } from "../bootstrap" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { EOL } from "os" +import { t } from "../../i18n" export const ExportCommand = cmd({ command: "export [sessionID]", @@ -18,11 +19,11 @@ export const ExportCommand = cmd({ handler: async (args) => { await bootstrap(process.cwd(), async () => { let sessionID = args.sessionID - process.stderr.write(`Exporting session: ${sessionID ?? "latest"}`) + process.stderr.write(t("export.exporting_session", { id: sessionID ?? t("export.latest") })) if (!sessionID) { UI.empty() - prompts.intro("Export session", { + prompts.intro(t("session.export"), { output: process.stderr, }) @@ -32,10 +33,10 @@ export const ExportCommand = cmd({ } if (sessions.length === 0) { - prompts.log.error("No sessions found", { + prompts.log.error(t("export.no_sessions"), { output: process.stderr, }) - prompts.outro("Done", { + prompts.outro(t("upgrade.done"), { output: process.stderr, }) return @@ -44,7 +45,7 @@ export const ExportCommand = cmd({ sessions.sort((a, b) => b.time.updated - a.time.updated) const selectedSession = await prompts.autocomplete({ - message: "Select session to export", + message: t("export.select_session"), maxItems: 10, options: sessions.map((session) => ({ label: session.title, @@ -60,7 +61,7 @@ export const ExportCommand = cmd({ sessionID = selectedSession as string - prompts.outro("Exporting session...", { + prompts.outro(t("export.exporting"), { output: process.stderr, }) } @@ -80,7 +81,7 @@ export const ExportCommand = cmd({ process.stdout.write(JSON.stringify(exportData, null, 2)) process.stdout.write(EOL) } catch (error) { - UI.error(`Session not found: ${sessionID!}`) + UI.error(t("export.session_not_found", { id: sessionID! })) process.exit(1) } }) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 63069d74e4b..72ecb809c70 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -11,6 +11,7 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider" import { Config } from "../../config/config" import { Instance } from "../../project/instance" import { Installation } from "../../installation" +import { t } from "../../i18n" import path from "path" import { Global } from "../../global" @@ -28,11 +29,11 @@ function getAuthStatusIcon(status: MCP.AuthStatus): string { function getAuthStatusText(status: MCP.AuthStatus): string { switch (status) { case "authenticated": - return "authenticated" + return t("mcp.status_authenticated") case "expired": - return "expired" + return t("mcp.status_expired") case "not_authenticated": - return "not authenticated" + return t("mcp.status_not_authenticated") } } @@ -70,7 +71,7 @@ export const McpListCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("MCP Servers") + prompts.intro(t("mcp.servers")) const config = await Config.get() const mcpServers = config.mcp ?? {} @@ -81,8 +82,8 @@ export const McpListCommand = cmd({ ) if (servers.length === 0) { - prompts.log.warn("No MCP servers configured") - prompts.outro("Add servers with: opencode mcp add") + prompts.log.warn(t("mcp.no_servers")) + prompts.outro(t("mcp.add_servers_hint")) return } @@ -97,26 +98,26 @@ export const McpListCommand = cmd({ if (!status) { statusIcon = "○" - statusText = "not initialized" + statusText = t("mcp.status_not_init") } else if (status.status === "connected") { statusIcon = "✓" - statusText = "connected" + statusText = t("mcp.status_connected") if (hasOAuth && hasStoredTokens) { hint = " (OAuth)" } } else if (status.status === "disabled") { statusIcon = "○" - statusText = "disabled" + statusText = t("mcp.status_disabled") } else if (status.status === "needs_auth") { statusIcon = "⚠" - statusText = "needs authentication" + statusText = t("mcp.status_needs_auth") } else if (status.status === "needs_client_registration") { statusIcon = "✗" - statusText = "needs client registration" + statusText = t("mcp.status_needs_client_reg") hint = "\n " + status.error } else { statusIcon = "✗" - statusText = "failed" + statusText = t("mcp.status_failed") hint = "\n " + status.error } @@ -126,7 +127,7 @@ export const McpListCommand = cmd({ ) } - prompts.outro(`${servers.length} server(s)`) + prompts.outro(t("mcp.server_count", { count: String(servers.length) })) }, }) }, @@ -147,7 +148,7 @@ export const McpAuthCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("MCP OAuth Authentication") + prompts.intro(t("mcp.oauth_auth")) const config = await Config.get() const mcpServers = config.mcp ?? {} @@ -158,8 +159,8 @@ export const McpAuthCommand = cmd({ ) if (oauthServers.length === 0) { - prompts.log.warn("No OAuth-capable MCP servers configured") - prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:") + prompts.log.warn(t("mcp.no_oauth_servers")) + prompts.log.info(t("mcp.oauth_remote_info")) prompts.log.info(` "mcp": { "my-server": { @@ -167,7 +168,7 @@ export const McpAuthCommand = cmd({ "url": "https://example.com/mcp" } }`) - prompts.outro("Done") + prompts.outro(t("upgrade.done")) return } @@ -189,7 +190,7 @@ export const McpAuthCommand = cmd({ ) const selected = await prompts.select({ - message: "Select MCP server to authenticate", + message: t("mcp.select_server_auth"), options, }) if (prompts.isCancel(selected)) throw new UI.CancelledError() @@ -198,14 +199,14 @@ export const McpAuthCommand = cmd({ const serverConfig = mcpServers[serverName] if (!serverConfig) { - prompts.log.error(`MCP server not found: ${serverName}`) - prompts.outro("Done") + prompts.log.error(t("mcp.server_not_found", { name: serverName })) + prompts.outro(t("upgrade.done")) return } if (!isMcpRemote(serverConfig) || serverConfig.oauth === false) { - prompts.log.error(`MCP server ${serverName} is not an OAuth-capable remote server`) - prompts.outro("Done") + prompts.log.error(t("mcp.server_not_oauth", { name: serverName })) + prompts.outro(t("upgrade.done")) return } @@ -213,28 +214,28 @@ export const McpAuthCommand = cmd({ const authStatus = await MCP.getAuthStatus(serverName) if (authStatus === "authenticated") { const confirm = await prompts.confirm({ - message: `${serverName} already has valid credentials. Re-authenticate?`, + message: t("mcp.already_authenticated", { name: serverName }), }) if (prompts.isCancel(confirm) || !confirm) { - prompts.outro("Cancelled") + prompts.outro(t("mcp.cancelled")) return } } else if (authStatus === "expired") { - prompts.log.warn(`${serverName} has expired credentials. Re-authenticating...`) + prompts.log.warn(t("mcp.expired_credentials", { name: serverName })) } const spinner = prompts.spinner() - spinner.start("Starting OAuth flow...") + spinner.start(t("mcp.starting_oauth")) try { const status = await MCP.authenticate(serverName) if (status.status === "connected") { - spinner.stop("Authentication successful!") + spinner.stop(t("mcp.auth_successful")) } else if (status.status === "needs_client_registration") { - spinner.stop("Authentication failed", 1) + spinner.stop(t("mcp.auth_failed"), 1) prompts.log.error(status.error) - prompts.log.info("Add clientId to your MCP server config:") + prompts.log.info(t("mcp.add_client_id_hint")) prompts.log.info(` "mcp": { "${serverName}": { @@ -247,17 +248,17 @@ export const McpAuthCommand = cmd({ } }`) } else if (status.status === "failed") { - spinner.stop("Authentication failed", 1) + spinner.stop(t("mcp.auth_failed"), 1) prompts.log.error(status.error) } else { - spinner.stop("Unexpected status: " + status.status, 1) + spinner.stop(t("mcp.unexpected_status", { status: status.status }), 1) } } catch (error) { - spinner.stop("Authentication failed", 1) + spinner.stop(t("mcp.auth_failed"), 1) prompts.log.error(error instanceof Error ? error.message : String(error)) } - prompts.outro("Done") + prompts.outro(t("upgrade.done")) }, }) }, @@ -272,7 +273,7 @@ export const McpAuthListCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("MCP OAuth Status") + prompts.intro(t("mcp.oauth_status")) const config = await Config.get() const mcpServers = config.mcp ?? {} @@ -283,8 +284,8 @@ export const McpAuthListCommand = cmd({ ) if (oauthServers.length === 0) { - prompts.log.warn("No OAuth-capable MCP servers configured") - prompts.outro("Done") + prompts.log.warn(t("mcp.no_oauth_servers")) + prompts.outro(t("upgrade.done")) return } @@ -297,7 +298,7 @@ export const McpAuthListCommand = cmd({ prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`) } - prompts.outro(`${oauthServers.length} OAuth-capable server(s)`) + prompts.outro(t("mcp.oauth_server_count", { count: String(oauthServers.length) })) }, }) }, @@ -316,30 +317,30 @@ export const McpLogoutCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("MCP OAuth Logout") + prompts.intro(t("mcp.oauth_logout")) const authPath = path.join(Global.Path.data, "mcp-auth.json") const credentials = await McpAuth.all() const serverNames = Object.keys(credentials) if (serverNames.length === 0) { - prompts.log.warn("No MCP OAuth credentials stored") - prompts.outro("Done") + prompts.log.warn(t("mcp.no_oauth_credentials")) + prompts.outro(t("upgrade.done")) return } let serverName = args.name if (!serverName) { const selected = await prompts.select({ - message: "Select MCP server to logout", + message: t("mcp.select_server_logout"), options: serverNames.map((name) => { const entry = credentials[name] const hasTokens = !!entry.tokens const hasClient = !!entry.clientInfo let hint = "" - if (hasTokens && hasClient) hint = "tokens + client" - else if (hasTokens) hint = "tokens" - else if (hasClient) hint = "client registration" + if (hasTokens && hasClient) hint = t("mcp.tokens_and_client") + else if (hasTokens) hint = t("mcp.tokens") + else if (hasClient) hint = t("mcp.client_registration") return { label: name, value: name, @@ -352,14 +353,14 @@ export const McpLogoutCommand = cmd({ } if (!credentials[serverName]) { - prompts.log.error(`No credentials found for: ${serverName}`) - prompts.outro("Done") + prompts.log.error(t("mcp.no_credentials_for", { name: serverName })) + prompts.outro(t("upgrade.done")) return } await MCP.removeAuth(serverName) - prompts.log.success(`Removed OAuth credentials for ${serverName}`) - prompts.outro("Done") + prompts.log.success(t("mcp.removed_credentials", { name: serverName })) + prompts.outro(t("upgrade.done")) }, }) }, @@ -370,26 +371,26 @@ export const McpAddCommand = cmd({ describe: "add an MCP server", async handler() { UI.empty() - prompts.intro("Add MCP server") + prompts.intro(t("mcp.add_server")) const name = await prompts.text({ - message: "Enter MCP server name", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + message: t("mcp.enter_server_name"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(name)) throw new UI.CancelledError() const type = await prompts.select({ - message: "Select MCP server type", + message: t("mcp.select_server_type"), options: [ { - label: "Local", + label: t("mcp.type_local"), value: "local", - hint: "Run a local command", + hint: t("mcp.type_local_hint"), }, { - label: "Remote", + label: t("mcp.type_remote"), value: "remote", - hint: "Connect to a remote URL", + hint: t("mcp.type_remote_hint"), }, ], }) @@ -397,52 +398,52 @@ export const McpAddCommand = cmd({ if (type === "local") { const command = await prompts.text({ - message: "Enter command to run", + message: t("mcp.enter_command"), placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(command)) throw new UI.CancelledError() - prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`) - prompts.outro("MCP server added successfully") + prompts.log.info(t("mcp.local_configured", { name, command })) + prompts.outro(t("mcp.server_added")) return } if (type === "remote") { const url = await prompts.text({ - message: "Enter MCP server URL", + message: t("mcp.enter_url"), placeholder: "e.g., https://example.com/mcp", validate: (x) => { - if (!x) return "Required" - if (x.length === 0) return "Required" + if (!x) return t("auth.required") + if (x.length === 0) return t("auth.required") const isValid = URL.canParse(x) - return isValid ? undefined : "Invalid URL" + return isValid ? undefined : t("mcp.invalid_url") }, }) if (prompts.isCancel(url)) throw new UI.CancelledError() const useOAuth = await prompts.confirm({ - message: "Does this server require OAuth authentication?", + message: t("mcp.requires_oauth"), initialValue: false, }) if (prompts.isCancel(useOAuth)) throw new UI.CancelledError() if (useOAuth) { const hasClientId = await prompts.confirm({ - message: "Do you have a pre-registered client ID?", + message: t("mcp.has_client_id"), initialValue: false, }) if (prompts.isCancel(hasClientId)) throw new UI.CancelledError() if (hasClientId) { const clientId = await prompts.text({ - message: "Enter client ID", - validate: (x) => (x && x.length > 0 ? undefined : "Required"), + message: t("mcp.enter_client_id"), + validate: (x) => (x && x.length > 0 ? undefined : t("auth.required")), }) if (prompts.isCancel(clientId)) throw new UI.CancelledError() const hasSecret = await prompts.confirm({ - message: "Do you have a client secret?", + message: t("mcp.has_client_secret"), initialValue: false, }) if (prompts.isCancel(hasSecret)) throw new UI.CancelledError() @@ -450,14 +451,14 @@ export const McpAddCommand = cmd({ let clientSecret: string | undefined if (hasSecret) { const secret = await prompts.password({ - message: "Enter client secret", + message: t("mcp.enter_client_secret"), }) if (prompts.isCancel(secret)) throw new UI.CancelledError() clientSecret = secret } - prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`) - prompts.log.info("Add this to your opencode.json:") + prompts.log.info(t("mcp.remote_configured_oauth", { name, clientId })) + prompts.log.info(t("mcp.add_to_config")) prompts.log.info(` "mcp": { "${name}": { @@ -469,8 +470,8 @@ export const McpAddCommand = cmd({ } }`) } else { - prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`) - prompts.log.info("Add this to your opencode.json:") + prompts.log.info(t("mcp.remote_configured_dynamic", { name })) + prompts.log.info(t("mcp.add_to_config")) prompts.log.info(` "mcp": { "${name}": { @@ -487,11 +488,11 @@ export const McpAddCommand = cmd({ }) const transport = new StreamableHTTPClientTransport(new URL(url)) await client.connect(transport) - prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`) + prompts.log.info(t("mcp.remote_configured_url", { name, url })) } } - prompts.outro("MCP server added successfully") + prompts.outro(t("mcp.server_added")) }, }) @@ -509,7 +510,7 @@ export const McpDebugCommand = cmd({ directory: process.cwd(), async fn() { UI.empty() - prompts.intro("MCP OAuth Debug") + prompts.intro(t("mcp.oauth_debug")) const config = await Config.get() const mcpServers = config.mcp ?? {} @@ -517,52 +518,56 @@ export const McpDebugCommand = cmd({ const serverConfig = mcpServers[serverName] if (!serverConfig) { - prompts.log.error(`MCP server not found: ${serverName}`) - prompts.outro("Done") + prompts.log.error(t("mcp.server_not_found", { name: serverName })) + prompts.outro(t("upgrade.done")) return } if (!isMcpRemote(serverConfig)) { - prompts.log.error(`MCP server ${serverName} is not a remote server`) - prompts.outro("Done") + prompts.log.error(t("mcp.server_not_remote", { name: serverName })) + prompts.outro(t("upgrade.done")) return } if (serverConfig.oauth === false) { - prompts.log.warn(`MCP server ${serverName} has OAuth explicitly disabled`) - prompts.outro("Done") + prompts.log.warn(t("mcp.oauth_disabled", { name: serverName })) + prompts.outro(t("upgrade.done")) return } - prompts.log.info(`Server: ${serverName}`) - prompts.log.info(`URL: ${serverConfig.url}`) + prompts.log.info(t("mcp.server_label", { name: serverName })) + prompts.log.info(t("mcp.url_label", { url: serverConfig.url })) // Check stored auth status const authStatus = await MCP.getAuthStatus(serverName) - prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`) + prompts.log.info( + t("mcp.auth_status_label", { icon: getAuthStatusIcon(authStatus), status: getAuthStatusText(authStatus) }), + ) const entry = await McpAuth.get(serverName) if (entry?.tokens) { - prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`) + prompts.log.info(` ${t("mcp.access_token", { token: entry.tokens.accessToken.substring(0, 20) })}`) if (entry.tokens.expiresAt) { const expiresDate = new Date(entry.tokens.expiresAt * 1000) const isExpired = entry.tokens.expiresAt < Date.now() / 1000 - prompts.log.info(` Expires: ${expiresDate.toISOString()} ${isExpired ? "(EXPIRED)" : ""}`) + prompts.log.info( + ` ${t("mcp.expires", { date: expiresDate.toISOString() })} ${isExpired ? t("mcp.expired_label") : ""}`, + ) } if (entry.tokens.refreshToken) { - prompts.log.info(` Refresh token: present`) + prompts.log.info(` ${t("mcp.refresh_token_present")}`) } } if (entry?.clientInfo) { - prompts.log.info(` Client ID: ${entry.clientInfo.clientId}`) + prompts.log.info(` ${t("mcp.client_id_label", { clientId: entry.clientInfo.clientId })}`) if (entry.clientInfo.clientSecretExpiresAt) { const expiresDate = new Date(entry.clientInfo.clientSecretExpiresAt * 1000) - prompts.log.info(` Client secret expires: ${expiresDate.toISOString()}`) + prompts.log.info(` ${t("mcp.client_secret_expires", { date: expiresDate.toISOString() })}`) } } const spinner = prompts.spinner() - spinner.start("Testing connection...") + spinner.start(t("mcp.testing_connection")) // Test basic HTTP connectivity first try { @@ -584,16 +589,16 @@ export const McpDebugCommand = cmd({ }), }) - spinner.stop(`HTTP response: ${response.status} ${response.statusText}`) + spinner.stop(t("mcp.http_response", { status: String(response.status), statusText: response.statusText })) // Check for WWW-Authenticate header const wwwAuth = response.headers.get("www-authenticate") if (wwwAuth) { - prompts.log.info(`WWW-Authenticate: ${wwwAuth}`) + prompts.log.info(t("mcp.www_authenticate", { value: wwwAuth })) } if (response.status === 401) { - prompts.log.warn("Server returned 401 Unauthorized") + prompts.log.warn(t("mcp.server_401")) // Try to discover OAuth metadata const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined @@ -610,7 +615,7 @@ export const McpDebugCommand = cmd({ }, ) - prompts.log.info("Testing OAuth flow (without completing authorization)...") + prompts.log.info(t("mcp.testing_oauth")) // Try creating transport with auth provider to trigger discovery const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { @@ -623,47 +628,49 @@ export const McpDebugCommand = cmd({ version: Installation.VERSION, }) await client.connect(transport) - prompts.log.success("Connection successful (already authenticated)") + prompts.log.success(t("mcp.connection_successful")) await client.close() } catch (error) { if (error instanceof UnauthorizedError) { - prompts.log.info(`OAuth flow triggered: ${error.message}`) + prompts.log.info(t("mcp.oauth_triggered", { message: error.message })) // Check if dynamic registration would be attempted const clientInfo = await authProvider.clientInformation() if (clientInfo) { - prompts.log.info(`Client ID available: ${clientInfo.client_id}`) + prompts.log.info(t("mcp.client_id_available", { clientId: clientInfo.client_id })) } else { - prompts.log.info("No client ID - dynamic registration will be attempted") + prompts.log.info(t("mcp.no_client_id")) } } else { - prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`) + prompts.log.error( + t("mcp.connection_error", { error: error instanceof Error ? error.message : String(error) }), + ) } } } else if (response.status >= 200 && response.status < 300) { - prompts.log.success("Server responded successfully (no auth required or already authenticated)") + prompts.log.success(t("mcp.server_success")) const body = await response.text() try { const json = JSON.parse(body) if (json.result?.serverInfo) { - prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`) + prompts.log.info(t("mcp.server_info", { info: JSON.stringify(json.result.serverInfo) })) } } catch { // Not JSON, ignore } } else { - prompts.log.warn(`Unexpected status: ${response.status}`) + prompts.log.warn(t("mcp.unexpected_status", { status: String(response.status) })) const body = await response.text().catch(() => "") if (body) { prompts.log.info(`Response body: ${body.substring(0, 500)}`) } } } catch (error) { - spinner.stop("Connection failed", 1) + spinner.stop(t("mcp.connection_failed"), 1) prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`) } - prompts.outro("Debug complete") + prompts.outro(t("mcp.debug_complete")) }, }) }, diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index 156dae91c67..3b1d97e87c2 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -5,6 +5,7 @@ import { ModelsDev } from "../../provider/models" import { cmd } from "./cmd" import { UI } from "../ui" import { EOL } from "os" +import { t } from "../../i18n" export const ModelsCommand = cmd({ command: "models [provider]", @@ -28,7 +29,7 @@ export const ModelsCommand = cmd({ handler: async (args) => { if (args.refresh) { await ModelsDev.refresh() - UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_SUCCESS_BOLD + t("models.cache_refreshed") + UI.Style.TEXT_NORMAL) } await Instance.provide({ @@ -52,7 +53,7 @@ export const ModelsCommand = cmd({ if (args.provider) { const provider = providers[args.provider] if (!provider) { - UI.error(`Provider not found: ${args.provider}`) + UI.error(t("models.provider_not_found", { provider: args.provider })) return } diff --git a/packages/opencode/src/cli/cmd/pr.ts b/packages/opencode/src/cli/cmd/pr.ts index d6176572002..ff5c8d1dc8e 100644 --- a/packages/opencode/src/cli/cmd/pr.ts +++ b/packages/opencode/src/cli/cmd/pr.ts @@ -2,6 +2,7 @@ import { UI } from "../ui" import { cmd } from "./cmd" import { Instance } from "@/project/instance" import { $ } from "bun" +import { t } from "../../i18n" export const PrCommand = cmd({ command: "pr ", @@ -18,19 +19,19 @@ export const PrCommand = cmd({ async fn() { const project = Instance.project if (project.vcs !== "git") { - UI.error("Could not find git repository. Please run this command from a git repository.") + UI.error(t("pr.no_git_repo")) process.exit(1) } const prNumber = args.number const localBranchName = `pr/${prNumber}` - UI.println(`Fetching and checking out PR #${prNumber}...`) + UI.println(t("pr.fetching", { number: String(prNumber) })) // Use gh pr checkout with custom branch name const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow() if (result.exitCode !== 0) { - UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`) + UI.error(t("pr.checkout_failed", { number: String(prNumber) })) process.exit(1) } @@ -55,7 +56,7 @@ export const PrCommand = cmd({ const remotes = (await $`git remote`.nothrow().text()).trim() if (!remotes.split("\n").includes(remoteName)) { await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow() - UI.println(`Added fork remote: ${remoteName}`) + UI.println(t("pr.added_fork_remote", { name: remoteName })) } // Set upstream to the fork so pushes go there @@ -68,8 +69,8 @@ export const PrCommand = cmd({ const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/) if (sessionMatch) { const sessionUrl = sessionMatch[0] - UI.println(`Found opencode session: ${sessionUrl}`) - UI.println(`Importing session...`) + UI.println(t("pr.found_session", { url: sessionUrl })) + UI.println(t("pr.importing_session")) const importResult = await $`opencode import ${sessionUrl}`.nothrow() if (importResult.exitCode === 0) { @@ -78,7 +79,7 @@ export const PrCommand = cmd({ const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/) if (sessionIdMatch) { sessionId = sessionIdMatch[1] - UI.println(`Session imported: ${sessionId}`) + UI.println(t("pr.session_imported", { id: sessionId })) } } } @@ -86,9 +87,9 @@ export const PrCommand = cmd({ } } - UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`) + UI.println(t("pr.checkout_success", { number: String(prNumber), branch: localBranchName })) UI.println() - UI.println("Starting opencode...") + UI.println(t("pr.starting")) UI.println() // Launch opencode TUI with session ID if available diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 876b64bd82a..dcd6e3b9633 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -11,6 +11,7 @@ import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" import { Agent } from "../../agent/agent" +import { t } from "../../i18n" const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], @@ -102,11 +103,11 @@ export const RunCommand = cmd({ const file = Bun.file(resolvedPath) const stats = await file.stat().catch(() => {}) if (!stats) { - UI.error(`File not found: ${filePath}`) + UI.error(t("run.file_not_found", { path: filePath })) process.exit(1) } if (!(await file.exists())) { - UI.error(`File not found: ${filePath}`) + UI.error(t("run.file_not_found", { path: filePath })) process.exit(1) } @@ -125,7 +126,7 @@ export const RunCommand = cmd({ if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text()) if (message.trim().length === 0 && !args.command) { - UI.error("You must provide a message or a command") + UI.error(t("run.must_provide_message")) process.exit(1) } @@ -206,11 +207,11 @@ export const RunCommand = cmd({ const permission = event.properties if (permission.sessionID !== sessionID) continue const result = await select({ - message: `Permission required: ${permission.permission} (${permission.patterns.join(", ")})`, + message: `${t("permission.title")}: ${permission.permission} (${permission.patterns.join(", ")})`, options: [ - { value: "once", label: "Allow once" }, - { value: "always", label: "Always allow: " + permission.always.join(", ") }, - { value: "reject", label: "Reject" }, + { value: "once", label: t("permission.allow_once") }, + { value: "always", label: t("permission.allow_always") + ": " + permission.always.join(", ") }, + { value: "reject", label: t("permission.reject") }, ], initialValue: "once", }).catch(() => "reject") @@ -232,7 +233,7 @@ export const RunCommand = cmd({ UI.println( UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL, - `agent "${args.agent}" not found. Falling back to default agent`, + t("run.agent_not_found", { name: args.agent }), ) return undefined } @@ -240,7 +241,7 @@ export const RunCommand = cmd({ UI.println( UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL, - `agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`, + t("run.agent_is_subagent", { name: args.agent }), ) return undefined } @@ -291,7 +292,7 @@ export const RunCommand = cmd({ })() if (!sessionID) { - UI.error("Session not found") + UI.error(t("run.session_not_found")) process.exit(1) } @@ -319,7 +320,7 @@ export const RunCommand = cmd({ const exists = await Command.get(args.command) if (!exists) { server.stop() - UI.error(`Command "${args.command}" not found`) + UI.error(t("run.command_not_found", { command: args.command })) process.exit(1) } } @@ -344,7 +345,7 @@ export const RunCommand = cmd({ if (!sessionID) { server.stop() - UI.error("Session not found") + UI.error(t("run.session_not_found")) process.exit(1) } diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 006d04f1cb3..12e3f3d33a6 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -14,6 +14,7 @@ import { DialogModel, useConnected } from "@tui/component/dialog-model" import { DialogMcp } from "@tui/component/dialog-mcp" import { DialogStatus } from "@tui/component/dialog-status" import { DialogThemeList } from "@tui/component/dialog-theme-list" +import { DialogLanguage } from "@tui/component/dialog-language" import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" @@ -35,6 +36,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { writeHeapSnapshot } from "v8" import { PromptRefProvider, usePromptRef } from "./context/prompt" +import { t, setLanguage, type SupportedLocale } from "@/i18n" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { // can't set raw mode if not a TTY @@ -189,7 +191,7 @@ function App() { // @ts-expect-error writeOut is not in type definitions renderer.writeOut(finalOsc52) await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .then(() => toast.show({ message: t("toast.copied_to_clipboard"), variant: "info" })) .catch(toast.error) renderer.clearSelection() } @@ -199,6 +201,11 @@ function App() { console.log(JSON.stringify(route.data)) }) + createEffect(() => { + const lang = (sync.data.config as { language?: SupportedLocale }).language + if (lang) setLanguage(lang) + }) + // Update terminal window title based on current route and session createEffect(() => { if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return @@ -271,24 +278,23 @@ function App() { const connected = useConnected() command.register(() => [ { - title: "Switch session", + title: t("command.switch_session"), value: "session.list", keybind: "session_list", - category: "Session", + category: t("category.session"), suggested: sync.data.session.length > 0, onSelect: () => { dialog.replace(() => ) }, }, { - title: "New session", + title: t("command.new_session"), suggested: route.data.type === "session", value: "session.new", keybind: "session_new", - category: "Session", + category: t("category.session"), onSelect: () => { const current = promptRef.current - // Don't require focus - if there's any text, preserve it const currentPrompt = current?.current?.input ? current.current : undefined route.navigate({ type: "home", @@ -298,169 +304,177 @@ function App() { }, }, { - title: "Switch model", + title: t("command.switch_model"), value: "model.list", keybind: "model_list", suggested: true, - category: "Agent", + category: t("category.agent"), onSelect: () => { dialog.replace(() => ) }, }, { - title: "Model cycle", + title: t("command.model_cycle"), disabled: true, value: "model.cycle_recent", keybind: "model_cycle_recent", - category: "Agent", + category: t("category.agent"), onSelect: () => { local.model.cycle(1) }, }, { - title: "Model cycle reverse", + title: t("command.model_cycle_reverse"), disabled: true, value: "model.cycle_recent_reverse", keybind: "model_cycle_recent_reverse", - category: "Agent", + category: t("category.agent"), onSelect: () => { local.model.cycle(-1) }, }, { - title: "Favorite cycle", + title: t("command.favorite_cycle"), value: "model.cycle_favorite", keybind: "model_cycle_favorite", - category: "Agent", + category: t("category.agent"), onSelect: () => { local.model.cycleFavorite(1) }, }, { - title: "Favorite cycle reverse", + title: t("command.favorite_cycle_reverse"), value: "model.cycle_favorite_reverse", keybind: "model_cycle_favorite_reverse", - category: "Agent", + category: t("category.agent"), onSelect: () => { local.model.cycleFavorite(-1) }, }, { - title: "Switch agent", + title: t("command.switch_agent"), value: "agent.list", keybind: "agent_list", - category: "Agent", + category: t("category.agent"), onSelect: () => { dialog.replace(() => ) }, }, { - title: "Toggle MCPs", + title: t("command.toggle_mcps"), value: "mcp.list", - category: "Agent", + category: t("category.agent"), onSelect: () => { dialog.replace(() => ) }, }, { - title: "Agent cycle", + title: t("command.agent_cycle"), value: "agent.cycle", keybind: "agent_cycle", - category: "Agent", + category: t("category.agent"), disabled: true, onSelect: () => { local.agent.move(1) }, }, { - title: "Variant cycle", + title: t("command.variant_cycle"), value: "variant.cycle", keybind: "variant_cycle", - category: "Agent", + category: t("category.agent"), onSelect: () => { local.model.variant.cycle() }, }, { - title: "Agent cycle reverse", + title: t("command.agent_cycle_reverse"), value: "agent.cycle.reverse", keybind: "agent_cycle_reverse", - category: "Agent", + category: t("category.agent"), disabled: true, onSelect: () => { local.agent.move(-1) }, }, { - title: "Connect provider", + title: t("command.connect_provider"), value: "provider.connect", suggested: !connected(), onSelect: () => { dialog.replace(() => ) }, - category: "Provider", + category: t("category.provider"), }, { - title: "View status", + title: t("command.view_status"), keybind: "status_view", value: "opencode.status", onSelect: () => { dialog.replace(() => ) }, - category: "System", + category: t("category.system"), }, { - title: "Switch theme", + title: t("command.switch_theme"), value: "theme.switch", onSelect: () => { dialog.replace(() => ) }, - category: "System", + category: t("category.system"), + }, + { + title: t("command.switch_language"), + value: "language.switch", + onSelect: () => { + dialog.replace(() => ) + }, + category: t("category.system"), }, { - title: "Toggle appearance", + title: t("command.toggle_appearance"), value: "theme.switch_mode", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") dialog.clear() }, - category: "System", + category: t("category.system"), }, { - title: "Help", + title: t("command.help"), value: "help.show", onSelect: () => { dialog.replace(() => ) }, - category: "System", + category: t("category.system"), }, { - title: "Open docs", + title: t("command.open_docs"), value: "docs.open", onSelect: () => { open("https://opencode.ai/docs").catch(() => {}) dialog.clear() }, - category: "System", + category: t("category.system"), }, { - title: "Open WebUI", + title: t("command.open_webui"), value: "webui.open", onSelect: () => { open(sdk.url).catch(() => {}) dialog.clear() }, - category: "System", + category: t("category.system"), }, { - title: "Exit the app", + title: t("command.exit_app"), value: "app.exit", onSelect: () => exit(), - category: "System", + category: t("category.system"), }, { - title: "Toggle debug panel", - category: "System", + title: t("command.toggle_debug"), + category: t("category.system"), value: "app.debug", onSelect: (dialog) => { renderer.toggleDebugOverlay() @@ -468,8 +482,8 @@ function App() { }, }, { - title: "Toggle console", - category: "System", + title: t("command.toggle_console"), + category: t("category.system"), value: "app.console", onSelect: (dialog) => { renderer.console.toggle() @@ -477,8 +491,8 @@ function App() { }, }, { - title: "Write heap snapshot", - category: "System", + title: t("command.heap_snapshot"), + category: t("category.system"), value: "app.heap_snapshot", onSelect: (dialog) => { const path = writeHeapSnapshot() @@ -491,25 +505,24 @@ function App() { }, }, { - title: "Suspend terminal", + title: t("command.suspend_terminal"), value: "terminal.suspend", keybind: "terminal_suspend", - category: "System", + category: t("category.system"), onSelect: () => { process.once("SIGCONT", () => { renderer.resume() }) renderer.suspend() - // pid=0 means send the signal to all processes in the process group process.kill(0, "SIGTSTP") }, }, { - title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title", + title: terminalTitleEnabled() ? t("command.disable_terminal_title") : t("command.enable_terminal_title"), value: "terminal.title.toggle", keybind: "terminal_title_toggle", - category: "System", + category: t("category.system"), onSelect: (dialog) => { setTerminalTitleEnabled((prev) => { const next = !prev @@ -527,11 +540,9 @@ function App() { if (!currentModel) return if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) { untrack(() => { - DialogAlert.show( - dialog, - "Warning", - "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", - ).then(() => kv.set("openrouter_warning", true)) + DialogAlert.show(dialog, t("warning.title"), t("warning.openrouter")).then(() => + kv.set("openrouter_warning", true), + ) }) } }) @@ -561,7 +572,7 @@ function App() { route.navigate({ type: "home" }) toast.show({ variant: "info", - message: "The current session was deleted", + message: t("toast.session_deleted"), }) } }) @@ -569,7 +580,7 @@ function App() { sdk.event.on(SessionApi.Event.Error.type, (evt) => { const error = evt.properties.error const message = (() => { - if (!error) return "An error occurred" + if (!error) return t("error.occurred") if (typeof error === "object") { const data = error.data @@ -590,8 +601,8 @@ function App() { sdk.event.on(Installation.Event.Updated.type, (evt) => { toast.show({ variant: "success", - title: "Update Complete", - message: `OpenCode updated to v${evt.properties.version}`, + title: t("update.complete"), + message: t("update.message_complete", evt.properties.version), duration: 5000, }) }) @@ -599,8 +610,8 @@ function App() { sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { toast.show({ variant: "info", - title: "Update Available", - message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`, + title: t("update.available"), + message: t("update.message_available", evt.properties.version), duration: 10000, }) }) @@ -623,7 +634,7 @@ function App() { /* @ts-expect-error */ renderer.writeOut(finalOsc52) await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .then(() => toast.show({ message: t("toast.copied_to_clipboard"), variant: "info" })) .catch(toast.error) renderer.clearSelection() } @@ -693,18 +704,18 @@ function ErrorComponent(props: { - Copy issue URL (exception info pre-filled) + {t("ui.copy_issue_url")} - {copied() && Successfully copied} + {copied() && {t("ui.successfully_copied")}} - A fatal error occurred! + {t("ui.fatal_error")} - Reset TUI + {t("ui.reset_tui")} - Exit + {t("ui.exit")} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx index 365a22445b4..de913926bb9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx @@ -2,6 +2,7 @@ import { createMemo } from "solid-js" import { useLocal } from "@tui/context/local" import { DialogSelect } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" +import { t } from "@/i18n" export function DialogAgent() { const local = useLocal() @@ -19,7 +20,7 @@ export function DialogAgent() { return ( { diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index d19e93188b2..0873f1699ec 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -12,6 +12,7 @@ import { import { useKeyboard } from "@opentui/solid" import { useKeybind } from "@tui/context/keybind" import type { KeybindsConfig } from "@opencode-ai/sdk/v2" +import { t } from "@/i18n" type Context = ReturnType const ctx = createContext() @@ -32,7 +33,7 @@ function init() { return [ ...suggested.map((x) => ({ ...x, - category: "Suggested", + category: t("command.suggested"), value: "suggested." + x.value, })), ...all, @@ -117,7 +118,7 @@ function DialogCommand(props: { options: CommandOption[] }) { return ( (ref = r)} - title="Commands" + title={t("command.category")} options={props.options.filter((x) => !ref?.filter || !x.value.startsWith("suggested."))} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-language.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-language.tsx new file mode 100644 index 00000000000..14199966fc6 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-language.tsx @@ -0,0 +1,34 @@ +import { useDialog } from "@tui/ui/dialog" +import { DialogSelect } from "@tui/ui/dialog-select" +import { getLanguage, setLanguage, t, type SupportedLocale } from "@/i18n" +import { useToast } from "@tui/ui/toast" + +const LANGUAGES: Array<{ value: SupportedLocale; title: string; native: string }> = [ + { value: "en", title: "English", native: "English" }, + { value: "ko", title: "Korean", native: "한국어" }, +] + +export function DialogLanguage() { + const dialog = useDialog() + const toast = useToast() + const current = getLanguage() + + return ( + ({ + title: `${lang.native} (${lang.title})`, + value: lang.value, + onSelect: () => { + setLanguage(lang.value) + dialog.clear() + toast.show({ + variant: "success", + message: t("language.changed", { language: lang.native }), + }) + }, + }))} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index 9cfa30d4df9..85b3590cd23 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -7,16 +7,17 @@ import { useTheme } from "../context/theme" import { Keybind } from "@/util/keybind" import { TextAttributes } from "@opentui/core" import { useSDK } from "@tui/context/sdk" +import { t } from "@/i18n" function Status(props: { enabled: boolean; loading: boolean }) { const { theme } = useTheme() if (props.loading) { - return ⋯ Loading + return ⋯ {t("mcp.loading")} } if (props.enabled) { - return ✓ Enabled + return ✓ {t("mcp.enabled")} } - return ○ Disabled + return ○ {t("mcp.disabled_status")} } export function DialogMcp() { @@ -48,7 +49,7 @@ export function DialogMcp() { const keybinds = createMemo(() => [ { keybind: Keybind.parse("space")[0], - title: "toggle", + title: t("mcp.toggle"), onTrigger: async (option: DialogSelectOption) => { // Prevent toggling while an operation is already in progress if (loading() !== null) return @@ -75,7 +76,7 @@ export function DialogMcp() { return ( { diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 50cf43896a9..dd707da5d99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -7,6 +7,7 @@ import { useDialog } from "@tui/ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { Keybind } from "@/util/keybind" import * as fuzzysort from "fuzzysort" +import { t } from "@/i18n" export function useConnected() { const sync = useSync() @@ -56,9 +57,9 @@ export function DialogModel(props: { providerID?: string }) { }, title: model.name ?? item.modelID, description: provider.name, - category: "Favorites", + category: t("category.favorites"), disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: model.cost?.input === 0 && provider.id === "opencode" ? t("model.free") : undefined, onSelect: () => { dialog.clear() local.model.set( @@ -87,9 +88,9 @@ export function DialogModel(props: { providerID?: string }) { }, title: model.name ?? item.modelID, description: provider.name, - category: "Recent", + category: t("category.recent"), disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: model.cost?.input === 0 && provider.id === "opencode" ? t("model.free") : undefined, onSelect: () => { dialog.clear() local.model.set( @@ -127,11 +128,11 @@ export function DialogModel(props: { providerID?: string }) { description: favorites.some( (item) => item.providerID === value.providerID && item.modelID === value.modelID, ) - ? "(Favorite)" + ? t("model.favorite") : undefined, category: connected() ? provider.name : undefined, disabled: provider.id === "opencode" && model.includes("-nano"), - footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: info.cost?.input === 0 && provider.id === "opencode" ? t("model.free") : undefined, onSelect() { dialog.clear() local.model.set( @@ -157,7 +158,7 @@ export function DialogModel(props: { providerID?: string }) { return true }), sortBy( - (x) => x.footer !== "Free", + (x) => x.footer !== t("model.free"), (x) => x.title, ), ), @@ -170,7 +171,7 @@ export function DialogModel(props: { providerID?: string }) { map((option) => { return { ...option, - category: "Popular providers", + category: t("category.popular_providers"), } }), take(6), @@ -198,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) { const title = createMemo(() => { if (provider()) return provider()!.name - return "Select model" + return t("model.select") }) return ( @@ -206,14 +207,14 @@ export function DialogModel(props: { providerID?: string }) { keybind={[ { keybind: Keybind.parse("ctrl+a")[0], - title: connected() ? "Connect provider" : "View all providers", + title: connected() ? t("action.connect_provider") : t("action.view_all_providers"), onTrigger() { dialog.replace(() => ) }, }, { keybind: Keybind.parse("ctrl+f")[0], - title: "Favorite", + title: t("action.favorite"), disabled: !connected(), onTrigger: (option) => { local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index d976485319f..dc0e20259a8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -10,6 +10,7 @@ import { useTheme } from "../context/theme" import { TextAttributes } from "@opentui/core" import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2" import { DialogModel } from "./dialog-model" +import { t } from "@/i18n" const PROVIDER_PRIORITY: Record = { opencode: 0, @@ -32,10 +33,10 @@ export function createDialogProviderOptions() { title: provider.name, value: provider.id, description: { - opencode: "(Recommended)", - anthropic: "(Claude Max or API key)", + opencode: t("provider.recommended"), + anthropic: t("provider.claude_max_or_api"), }[provider.id], - category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", + category: provider.id in PROVIDER_PRIORITY ? t("provider.category_popular") : t("provider.category_other"), async onSelect() { const methods = sync.data.provider_auth[provider.id] ?? [ { @@ -49,7 +50,7 @@ export function createDialogProviderOptions() { dialog.replace( () => ( ({ title: x.label, value: index, @@ -91,7 +92,7 @@ export function createDialogProviderOptions() { export function DialogProvider() { const options = createDialogProviderOptions() - return + return } interface AutoMethodProps { @@ -132,7 +133,7 @@ function AutoMethod(props: AutoMethodProps) { {props.authorization.instructions} - Waiting for authorization... + {t("ui.waiting_for_auth")} ) } @@ -153,7 +154,7 @@ function CodeMethod(props: CodeMethodProps) { return ( { const { error } = await sdk.client.provider.oauth.callback({ providerID: props.providerID, @@ -173,7 +174,7 @@ function CodeMethod(props: CodeMethodProps) { {props.authorization.instructions} - Invalid code + {t("ui.invalid_code")} )} @@ -194,15 +195,14 @@ function ApiMethod(props: ApiMethodProps) { return ( - - OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key. - + {t("zen.description")} - Go to https://opencode.ai/zen to get a key + {t("zen.get_key", "https://opencode.ai/zen")}{" "} + https://opencode.ai/zen ) : undefined diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index cb7b5d282ee..b27212e8879 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -10,6 +10,7 @@ import { useSDK } from "../context/sdk" import { DialogSessionRename } from "./dialog-session-rename" import { useKV } from "../context/kv" import "opentui-spinner/solid" +import { t } from "@/i18n" export function DialogSessionList() { const dialog = useDialog() @@ -36,13 +37,13 @@ export function DialogSessionList() { const date = new Date(x.time.updated) let category = date.toDateString() if (category === today) { - category = "Today" + category = t("time.today") } const isDeleting = toDelete() === x.id const status = sync.data.session_status?.[x.id] const isWorking = status?.type === "busy" return { - title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title, + title: isDeleting ? t("dialog.press_again_to_confirm", deleteKeybind) : x.title, bg: isDeleting ? theme.error : undefined, value: x.id, category, @@ -67,7 +68,7 @@ export function DialogSessionList() { return ( { @@ -83,7 +84,7 @@ export function DialogSessionList() { keybind={[ { keybind: Keybind.parse(deleteKeybind)[0], - title: "delete", + title: t("session.delete"), onTrigger: async (option) => { if (toDelete() === option.value) { sdk.client.session.delete({ @@ -97,7 +98,7 @@ export function DialogSessionList() { }, { keybind: Keybind.parse("ctrl+r")[0], - title: "rename", + title: t("session.rename"), onTrigger: async (option) => { dialog.replace(() => ) }, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx index 141340d5562..5a50e032d56 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx @@ -3,6 +3,7 @@ import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" import { createMemo } from "solid-js" import { useSDK } from "../context/sdk" +import { t } from "@/i18n" interface DialogSessionRenameProps { session: string @@ -16,7 +17,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) { return ( { sdk.client.session.update({ diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx index 29f2d78dca9..ea7223f86a7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx @@ -5,6 +5,7 @@ import { Locale } from "@/util/locale" import { Keybind } from "@/util/keybind" import { useTheme } from "../context/theme" import { usePromptStash, type StashEntry } from "./prompt/stash" +import { t } from "@/i18n" function getRelativeTime(timestamp: number): string { const now = Date.now() @@ -14,10 +15,10 @@ function getRelativeTime(timestamp: number): string { const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) - if (seconds < 60) return "just now" - if (minutes < 60) return `${minutes}m ago` - if (hours < 24) return `${hours}h ago` - if (days < 7) return `${days}d ago` + if (seconds < 60) return t("time.just_now") + if (minutes < 60) return t("stash.minutes_ago", String(minutes)) + if (hours < 24) return t("stash.hours_ago", String(hours)) + if (days < 7) return t("time.days_ago", String(days)) return Locale.datetime(timestamp) } @@ -41,11 +42,11 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const isDeleting = toDelete() === index const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1 return { - title: isDeleting ? "Press ctrl+d again to confirm" : getStashPreview(entry.input), + title: isDeleting ? t("dialog.press_again_to_confirm", "ctrl+d") : getStashPreview(entry.input), bg: isDeleting ? theme.error : undefined, value: index, description: getRelativeTime(entry.timestamp), - footer: lineCount > 1 ? `~${lineCount} lines` : undefined, + footer: lineCount > 1 ? t("stash.lines", String(lineCount)) : undefined, } }) .toReversed() @@ -53,7 +54,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { return ( { setToDelete(undefined) @@ -70,7 +71,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { keybind={[ { keybind: Keybind.parse("ctrl+d")[0], - title: "delete", + title: t("session.delete"), onTrigger: (option) => { if (toDelete() === option.value) { stash.remove(option.value) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index b85cd5c6542..df85c931a4a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -2,6 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useSync } from "@tui/context/sync" import { For, Match, Switch, Show, createMemo } from "solid-js" +import { t } from "@/i18n" export type DialogStatusProps = {} @@ -40,13 +41,18 @@ export function DialogStatus() { - Status + {t("status.title")} esc - 0} fallback={No MCP Servers}> + 0} + fallback={{t("status.no_mcp_servers")}} + > - {Object.keys(sync.data.mcp).length} MCP Servers + + {Object.keys(sync.data.mcp).length} {t("status.mcp_servers")} + {([key, item]) => ( @@ -70,11 +76,11 @@ export function DialogStatus() { {key}{" "} - Connected + {t("ui.connected")} {(val) => val().error} - Disabled in configuration + {t("ui.disabled_in_config")} - Needs authentication (run: opencode mcp auth {key}) + {t("ui.needs_auth")} (run: opencode mcp auth {key}) {(val) => (val() as { error: string }).error} @@ -89,7 +95,9 @@ export function DialogStatus() { {sync.data.lsp.length > 0 && ( - {sync.data.lsp.length} LSP Servers + + {sync.data.lsp.length} {t("status.lsp_servers")} + {(item) => ( @@ -112,9 +120,11 @@ export function DialogStatus() { )} - 0} fallback={No Formatters}> + 0} fallback={{t("status.no_formatters")}}> - {enabledFormatters().length} Formatters + + {enabledFormatters().length} {t("status.formatters")} + {(item) => ( @@ -134,9 +144,11 @@ export function DialogStatus() { - 0} fallback={No Plugins}> + 0} fallback={{t("status.no_plugins")}}> - {plugins().length} Plugins + + {plugins().length} {t("status.plugins")} + {(item) => ( diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx index 6d6c62450ea..438fd5cbc8d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx @@ -3,6 +3,7 @@ import { DialogSelect } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" import { useSDK } from "@tui/context/sdk" import { createStore } from "solid-js/store" +import { t } from "@/i18n" export function DialogTag(props: { onSelect?: (value: string) => void }) { const sdk = useSDK() @@ -33,7 +34,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) { return ( { props.onSelect?.(option.value) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx index f4072c97858..4bf41cfb63c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -2,6 +2,7 @@ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" import { useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" import { onCleanup, onMount } from "solid-js" +import { t } from "@/i18n" export function DialogThemeList() { const theme = useTheme() @@ -22,7 +23,7 @@ export function DialogThemeList() { return ( { diff --git a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx b/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx index 36bf174c117..b0dcab8d8d8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/did-you-know.tsx @@ -1,8 +1,9 @@ import { createMemo, createSignal, For } from "solid-js" import { useTheme } from "@tui/context/theme" import { useKeybind } from "@tui/context/keybind" -import { TIPS } from "./tips" +import { getTips } from "./tips" import { EmptyBorder } from "./border" +import { t } from "@/i18n" type TipPart = { text: string; highlight: boolean } @@ -27,6 +28,7 @@ function parseTip(tip: string): TipPart[] { return parts } +const TIPS = getTips() const [tipIndex, setTipIndex] = createSignal(Math.floor(Math.random() * TIPS.length)) export function randomizeTip() { @@ -34,25 +36,25 @@ export function randomizeTip() { } const BOX_WIDTH = 42 -const TITLE = " 🅘 Did you know? " export function DidYouKnow() { const { theme } = useTheme() const keybind = useKeybind() const tipParts = createMemo(() => parseTip(TIPS[tipIndex()])) + const title = createMemo(() => ` 🅘 ${t("misc.did_you_know")} `) const dashes = createMemo(() => { // ╭─ + title + ─...─ + ╮ = BOX_WIDTH // 1 + 1 + title.length + dashes + 1 = BOX_WIDTH - return Math.max(0, BOX_WIDTH - 2 - TITLE.length - 1) + return Math.max(0, BOX_WIDTH - 2 - title().length - 1) }) return ( ╭─ - {TITLE} + {title()} {"─".repeat(dashes())}╮ {keybind.print("tips_toggle")} - hide tips + {t("misc.hide_tips")} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 2e68fdcd924..48b521c1d9e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -10,6 +10,7 @@ import { SplitBorder } from "@tui/component/border" import { useCommandDialog } from "@tui/component/dialog-command" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "@/util/locale" +import { t } from "@/i18n" import type { PromptInfo } from "./history" function removeLineRange(input: string) { @@ -274,56 +275,56 @@ export function Autocomplete(props: { results.push( { display: "/undo", - description: "undo the last message", + description: t("slash.undo"), onSelect: () => { command.trigger("session.undo") }, }, { display: "/redo", - description: "redo the last message", + description: t("slash.redo"), onSelect: () => command.trigger("session.redo"), }, { display: "/compact", aliases: ["/summarize"], - description: "compact the session", + description: t("slash.compact"), onSelect: () => command.trigger("session.compact"), }, { display: "/unshare", disabled: !s.share, - description: "unshare a session", + description: t("slash.unshare"), onSelect: () => command.trigger("session.unshare"), }, { display: "/rename", - description: "rename session", + description: t("slash.rename"), onSelect: () => command.trigger("session.rename"), }, { display: "/copy", - description: "copy session transcript to clipboard", + description: t("slash.copy"), onSelect: () => command.trigger("session.copy"), }, { display: "/export", - description: "export session transcript to file", + description: t("slash.export"), onSelect: () => command.trigger("session.export"), }, { display: "/timeline", - description: "jump to message", + description: t("slash.timeline"), onSelect: () => command.trigger("session.timeline"), }, { display: "/fork", - description: "fork from message", + description: t("slash.fork"), onSelect: () => command.trigger("session.fork"), }, { display: "/thinking", - description: "toggle thinking visibility", + description: t("slash.thinking"), onSelect: () => command.trigger("session.toggle.thinking"), }, ) @@ -331,7 +332,7 @@ export function Autocomplete(props: { results.push({ display: "/share", disabled: !!s.share?.url, - description: "share a session", + description: t("slash.share"), onSelect: () => command.trigger("session.share"), }) } @@ -341,64 +342,64 @@ export function Autocomplete(props: { { display: "/new", aliases: ["/clear"], - description: "create a new session", + description: t("slash.new"), onSelect: () => command.trigger("session.new"), }, { display: "/models", - description: "list models", + description: t("slash.models"), onSelect: () => command.trigger("model.list"), }, { display: "/agents", - description: "list agents", + description: t("slash.agents"), onSelect: () => command.trigger("agent.list"), }, { display: "/session", aliases: ["/resume", "/continue"], - description: "list sessions", + description: t("slash.sessions"), onSelect: () => command.trigger("session.list"), }, { display: "/status", - description: "show status", + description: t("slash.status"), onSelect: () => command.trigger("opencode.status"), }, { display: "/mcp", - description: "toggle MCPs", + description: t("slash.mcp"), onSelect: () => command.trigger("mcp.list"), }, { display: "/theme", - description: "toggle theme", + description: t("slash.theme"), onSelect: () => command.trigger("theme.switch"), }, { display: "/editor", - description: "open editor", + description: t("slash.editor"), onSelect: () => command.trigger("prompt.editor", "prompt"), }, { display: "/connect", - description: "connect to a provider", + description: t("slash.connect"), onSelect: () => command.trigger("provider.connect"), }, { display: "/help", - description: "show help", + description: t("slash.help"), onSelect: () => command.trigger("help.show"), }, { display: "/commands", - description: "show all commands", + description: t("slash.commands"), onSelect: () => command.show(), }, { display: "/exit", aliases: ["/quit", "/q"], - description: "exit the app", + description: t("slash.exit"), onSelect: () => command.trigger("app.exit"), }, ) @@ -592,7 +593,7 @@ export function Autocomplete(props: { each={options()} fallback={ - No matching items + {t("ui.no_matching_items")} } > diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index fcb920c3044..f0d2e645e6b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,4 +1,4 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core" +import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t as tuiT, dim, fg } from "@opentui/core" import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import { useLocal } from "@tui/context/local" @@ -30,6 +30,7 @@ import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" +import { t } from "@/i18n" export type PromptProps = { sessionID?: string @@ -76,7 +77,7 @@ export function Prompt(props: PromptProps) { function promptModelWarning() { toast.show({ variant: "warning", - message: "Connect a provider to send prompts", + message: t("prompt.connect_provider"), duration: 3000, }) if (sync.data.provider.length === 0) { @@ -153,9 +154,9 @@ export function Prompt(props: PromptProps) { command.register(() => { return [ { - title: "Clear prompt", + title: t("prompt.clear"), value: "prompt.clear", - category: "Prompt", + category: t("prompt.category"), disabled: true, onSelect: (dialog) => { input.extmarks.clear() @@ -164,11 +165,11 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Submit prompt", + title: t("prompt.submit"), value: "prompt.submit", disabled: true, keybind: "input_submit", - category: "Prompt", + category: t("prompt.category"), onSelect: (dialog) => { if (!input.focused) return submit() @@ -176,11 +177,11 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Paste", + title: t("prompt.paste"), value: "prompt.paste", disabled: true, keybind: "input_paste", - category: "Prompt", + category: t("prompt.category"), onSelect: async () => { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { @@ -193,11 +194,11 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Interrupt session", + title: t("prompt.interrupt"), value: "session.interrupt", keybind: "session_interrupt", disabled: status().type === "idle", - category: "Session", + category: t("session.category"), onSelect: (dialog) => { if (autocomplete.visible) return if (!input.focused) return @@ -224,8 +225,8 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Open editor", - category: "Session", + title: t("prompt.open_editor"), + category: t("session.category"), keybind: "editor_open", value: "prompt.editor", onSelect: async (dialog, trigger) => { @@ -399,9 +400,9 @@ export function Prompt(props: PromptProps) { command.register(() => [ { - title: "Stash prompt", + title: t("prompt.stash"), value: "prompt.stash", - category: "Prompt", + category: t("prompt.category"), disabled: !store.prompt.input, onSelect: (dialog) => { if (!store.prompt.input) return @@ -417,9 +418,9 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Stash pop", + title: t("prompt.stash_pop"), value: "prompt.stash.pop", - category: "Prompt", + category: t("prompt.category"), disabled: stash.list().length === 0, onSelect: (dialog) => { const entry = stash.pop() @@ -433,9 +434,9 @@ export function Prompt(props: PromptProps) { }, }, { - title: "Stash list", + title: t("prompt.stash_list"), value: "prompt.stash.list", - category: "Prompt", + category: t("prompt.category"), disabled: stash.list().length === 0, onSelect: (dialog) => { dialog.replace(() => ( @@ -927,7 +928,7 @@ export function Prompt(props: PromptProps) { /> - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} + {store.mode === "shell" ? t("misc.shell") : Locale.titlecase(local.agent.current().name)}{" "} @@ -1021,7 +1022,7 @@ export function Prompt(props: PromptProps) { const r = retry() if (!r) return if (isTruncated()) { - DialogAlert.show(dialog, "Retry Error", r.message) + DialogAlert.show(dialog, t("error.retry"), r.message) } } @@ -1047,7 +1048,7 @@ export function Prompt(props: PromptProps) { 0 ? theme.primary : theme.text}> esc{" "} 0 ? theme.primary : theme.textMuted }}> - {store.interrupt > 0 ? "again to interrupt" : "interrupt"} + {store.interrupt > 0 ? t("session.again_to_interrupt") : t("session.interrupt")} @@ -1057,15 +1058,15 @@ export function Prompt(props: PromptProps) { - {keybind.print("agent_cycle")} switch agent + {keybind.print("agent_cycle")} {t("agent.switch")} - {keybind.print("command_list")} commands + {keybind.print("command_list")} {t("command.list")} - esc exit shell mode + esc {t("misc.exit_shell")} diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.ts b/packages/opencode/src/cli/cmd/tui/component/tips.ts index ed8ce147471..fdb3ac48ca9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.ts +++ b/packages/opencode/src/cli/cmd/tui/component/tips.ts @@ -1,103 +1,8 @@ -export const TIPS = [ - "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files to your prompt.", - "Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight}).", - "Press {highlight}Tab{/highlight} to cycle between Build (full access) and Plan (read-only) agents.", - "Use {highlight}/undo{/highlight} to revert the last message and any file changes made by OpenCode.", - "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes.", - "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai.", - "Drag and drop images into the terminal to add them as context for your prompts.", - "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard directly into the prompt.", - "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor.", - "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase structure.", - "Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models.", - "Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to preview and switch between 50+ built-in themes.", - "Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session.", - "Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations.", - "Run {highlight}/compact{/highlight} to summarize long sessions when approaching context limits.", - "Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown.", - "Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard.", - "Press {highlight}Ctrl+P{/highlight} to see all available actions and commands.", - "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers.", - "The default leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions.", - "Press {highlight}F2{/highlight} to quickly switch between recently used models.", - "Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel.", - "Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history.", - "Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation.", - "Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message.", - "Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt.", - "Press {highlight}Ctrl+C{/highlight} when typing to clear the input field.", - "Press {highlight}Escape{/highlight} to stop the AI mid-response.", - "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes.", - "Use {highlight}@{/highlight} in prompts to invoke specialized subagents.", - "Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions.", - "Create {highlight}opencode.json{/highlight} in project root for project-specific settings.", - "Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config.", - "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor.", - "Configure {highlight}model{/highlight} in config to set your default model.", - "Override any keybind in config via the {highlight}keybinds{/highlight} section.", - "Set any keybind to {highlight}none{/highlight} to disable it completely.", - "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section.", - "OpenCode auto-handles OAuth for remote MCP servers requiring auth.", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts.", - "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input.", - "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight}).", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas.", - "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools.", - 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions.', - 'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands.', - 'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing.', - "OpenCode auto-formats files using prettier, gofmt, ruff, and more.", - 'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting.', - "Define custom formatter commands with file extensions in config.", - "OpenCode uses LSP servers for intelligent code analysis.", - "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tool/{/highlight} to define new LLM tools.", - "Tool definitions can invoke scripts written in Python, Go, etc.", - "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks.", - "Use plugins to send OS notifications when sessions complete.", - "Create a plugin to prevent OpenCode from reading sensitive files.", - "Use {highlight}opencode run{/highlight} for non-interactive scripting.", - "Use {highlight}opencode run --continue{/highlight} to resume the last session.", - "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI.", - "Use {highlight}--format json{/highlight} for machine-readable output in scripts.", - "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode.", - "Use {highlight}opencode run --attach{/highlight} to connect to a running server for faster runs.", - "Run {highlight}opencode upgrade{/highlight} to update to the latest version.", - "Run {highlight}opencode auth list{/highlight} to see all configured providers.", - "Run {highlight}opencode agent create{/highlight} for guided agent creation.", - "Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions.", - "Run {highlight}opencode github install{/highlight} to set up the GitHub workflow.", - "Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs.", - "Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews.", - 'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors.', - "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory.", - "Themes support dark/light variants for both modes.", - "Reference ANSI colors 0-255 in custom themes.", - "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config.", - "Use {highlight}{file:path}{/highlight} to include file contents in config values.", - "Use {highlight}instructions{/highlight} in config to load additional rules files.", - "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative).", - "Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request.", - 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools.', - 'Use {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server.', - "Override global tool settings per agent configuration.", - 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions.', - 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing.', - "Run {highlight}/unshare{/highlight} to remove a session from public access.", - "Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops.", - "Permission {highlight}external_directory{/highlight} protects files outside project.", - "Run {highlight}opencode debug config{/highlight} to troubleshoot configuration.", - "Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr.", - "Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages.", - "Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages.", - "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info.", - "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling.", - "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight}).", - "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use.", - "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models.", - "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing.", - "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs.", - "Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog.", - "Use {highlight}/details{/highlight} to toggle tool execution details visibility.", - "Use {highlight}/rename{/highlight} to rename the current session.", - "Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell.", -] +import { t } from "@/i18n" + +export function getTips(): string[] { + return Array.from({ length: 101 }, (_, i) => t(`tips.${i}`)) +} + +// For backward compatibility +export const TIPS = getTips() diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index b60a775b375..07b086d82c8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" +import { t } from "@/i18n" export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", @@ -202,8 +203,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const value = currentModel() if (!value) { return { - provider: "Connect a provider", - model: "No provider selected", + provider: t("model.connect_provider"), + model: t("model.no_provider_selected"), reasoning: false, } } @@ -233,7 +234,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (!favorites.length) { toast.show({ variant: "info", - message: "Add a favorite model to use this shortcut", + message: t("toast.add_favorite_model"), duration: 3000, }) return diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 2155ab94744..892ba76e280 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -13,6 +13,7 @@ import { usePromptRef } from "../context/prompt" import { Installation } from "@/installation" import { useKV } from "../context/kv" import { useCommandDialog } from "../component/dialog-command" +import { t } from "@/i18n" // TODO: what is the best way to do this? let once = false @@ -44,10 +45,10 @@ export function Home() { command.register(() => [ { - title: tipsHidden() ? "Show tips" : "Hide tips", + title: tipsHidden() ? t("misc.show_tips") : t("misc.hide_tips"), value: "tips.toggle", keybind: "tips_toggle", - category: "System", + category: t("category.system"), onSelect: (dialog) => { kv.set("tips_hidden", !tipsHidden()) dialog.clear() diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx index 62154cce563..bc998434ded 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx @@ -7,6 +7,7 @@ import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useDialog } from "../../ui/dialog" import type { PromptInfo } from "@tui/component/prompt/history" +import { t } from "@/i18n" export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) { const sync = useSync() @@ -60,5 +61,11 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess return result }) - return props.onMove(option.value)} title="Fork from message" options={options()} /> + return ( + props.onMove(option.value)} + title={t("dialog.fork_from_message")} + options={options()} + /> + ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index ff17b5567eb..152608a8d25 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -5,6 +5,7 @@ import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { Clipboard } from "@tui/util/clipboard" import type { PromptInfo } from "@tui/component/prompt/history" +import { t } from "@/i18n" export function DialogMessage(props: { messageID: string @@ -18,12 +19,12 @@ export function DialogMessage(props: { return ( { const msg = message() if (!msg) return @@ -52,9 +53,9 @@ export function DialogMessage(props: { }, }, { - title: "Copy", + title: t("action.copy"), value: "message.copy", - description: "message text to clipboard", + description: t("action.copy_desc"), onSelect: async (dialog) => { const msg = message() if (!msg) return @@ -72,9 +73,9 @@ export function DialogMessage(props: { }, }, { - title: "Fork", + title: t("action.fork"), value: "session.fork", - description: "create a new session", + description: t("action.fork_desc"), onSelect: async (dialog) => { const result = await sdk.client.session.fork({ sessionID: props.sessionID, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx index c5ef70ef06f..f19ce8df365 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx @@ -1,17 +1,18 @@ import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" +import { t } from "@/i18n" export function DialogSubagent(props: { sessionID: string }) { const route = useRoute() return ( { route.navigate({ type: "session", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx index 87248a6a8ba..1b14cd8f3e0 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx @@ -6,6 +6,7 @@ import { Locale } from "@/util/locale" import { DialogMessage } from "./dialog-message" import { useDialog } from "../../ui/dialog" import type { PromptInfo } from "../../component/prompt/history" +import { t } from "@/i18n" export function DialogTimeline(props: { sessionID: string @@ -43,5 +44,7 @@ export function DialogTimeline(props: { return result }) - return props.onMove(option.value)} title="Timeline" options={options()} /> + return ( + props.onMove(option.value)} title={t("dialog.timeline")} options={options()} /> + ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 098ee83cce8..6dc590c47d4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -7,6 +7,7 @@ import { SplitBorder, EmptyBorder } from "@tui/component/border" import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" import { useDirectory } from "../../context/directory" import { useKeybind } from "../../context/keybind" +import { t } from "@/i18n" const Title = (props: { session: Accessor }) => { const { theme } = useTheme() @@ -79,16 +80,17 @@ export function Header() { - Subagent session + {t("ui.subagent_session")} - Parent {keybind.print("session_parent")} + {t("session.parent")} {keybind.print("session_parent")} - Prev {keybind.print("session_child_cycle_reverse")} + {t("session.prev")}{" "} + {keybind.print("session_child_cycle_reverse")} - Next {keybind.print("session_child_cycle")} + {t("session.next")} {keybind.print("session_child_cycle")} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d049ec4373c..540ab35b31a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -28,6 +28,7 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2" import { useLocal } from "@tui/context/local" import { Locale } from "@/util/locale" +import { t } from "@/i18n" import type { Tool } from "@/tool/tool" import type { ReadTool } from "@/tool/read" import type { WriteTool } from "@/tool/write" @@ -263,12 +264,12 @@ export function Session() { ...(sync.data.config.share !== "disabled" ? [ { - title: "Share session", + title: t("session.share"), value: "session.share", suggested: route.type === "session", keybind: "session_share" as const, disabled: !!session()?.share?.url, - category: "Session", + category: t("session.category"), onSelect: async (dialog: any) => { await sdk.client.session .share({ @@ -276,30 +277,30 @@ export function Session() { }) .then((res) => Clipboard.copy(res.data!.share!.url).catch(() => - toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }), + toast.show({ message: t("toast.failed_to_copy"), variant: "error" }), ), ) - .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) - .catch(() => toast.show({ message: "Failed to share session", variant: "error" })) + .then(() => toast.show({ message: t("toast.share_url_copied"), variant: "success" })) + .catch(() => toast.show({ message: t("toast.failed_to_share"), variant: "error" })) dialog.clear() }, }, ] : []), { - title: "Rename session", + title: t("session.rename"), value: "session.rename", keybind: "session_rename", - category: "Session", + category: t("session.category"), onSelect: (dialog) => { dialog.replace(() => ) }, }, { - title: "Jump to message", + title: t("session.jump"), value: "session.timeline", keybind: "session_timeline", - category: "Session", + category: t("session.category"), onSelect: (dialog) => { dialog.replace(() => ( { dialog.replace(() => ( { const selectedModel = local.model.current() if (!selectedModel) { toast.show({ variant: "warning", - message: "Connect a provider to summarize this session", + message: t("toast.connect_provider_to_summarize"), duration: 3000, }) return @@ -358,26 +359,26 @@ export function Session() { }, }, { - title: "Unshare session", + title: t("command.unshare_session"), value: "session.unshare", keybind: "session_unshare", disabled: !session()?.share?.url, - category: "Session", + category: t("category.session"), onSelect: async (dialog) => { await sdk.client.session .unshare({ sessionID: route.sessionID, }) - .then(() => toast.show({ message: "Session unshared successfully", variant: "success" })) - .catch(() => toast.show({ message: "Failed to unshare session", variant: "error" })) + .then(() => toast.show({ message: t("toast.session_unshared"), variant: "success" })) + .catch(() => toast.show({ message: t("toast.failed_to_unshare"), variant: "error" })) dialog.clear() }, }, { - title: "Undo previous message", + title: t("session.undo"), value: "session.undo", keybind: "messages_undo", - category: "Session", + category: t("session.category"), onSelect: async (dialog) => { const status = sync.data.session_status?.[route.sessionID] if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {}) @@ -409,11 +410,11 @@ export function Session() { }, }, { - title: "Redo", + title: t("command.redo"), value: "session.redo", keybind: "messages_redo", disabled: !session()?.revert?.messageID, - category: "Session", + category: t("category.session"), onSelect: (dialog) => { dialog.clear() const messageID = session().revert?.messageID @@ -433,10 +434,10 @@ export function Session() { }, }, { - title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", + title: sidebarVisible() ? t("command.hide_sidebar") : t("command.show_sidebar"), value: "session.sidebar.toggle", keybind: "sidebar_toggle", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setSidebar((prev) => { if (prev === "auto") return sidebarVisible() ? "hide" : "show" @@ -449,10 +450,10 @@ export function Session() { }, }, { - title: usernameVisible() ? "Hide username" : "Show username", + title: usernameVisible() ? t("command.hide_username") : t("command.show_username"), value: "session.username_visible.toggle", keybind: "username_toggle", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setUsernameVisible((prev) => { const next = !prev @@ -463,19 +464,19 @@ export function Session() { }, }, { - title: "Toggle code concealment", + title: t("command.toggle_code_concealment"), value: "session.toggle.conceal", keybind: "messages_toggle_conceal" as any, - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setConceal((prev) => !prev) dialog.clear() }, }, { - title: showTimestamps() ? "Hide timestamps" : "Show timestamps", + title: showTimestamps() ? t("command.hide_timestamps") : t("command.show_timestamps"), value: "session.toggle.timestamps", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setShowTimestamps((prev) => { const next = !prev @@ -486,9 +487,9 @@ export function Session() { }, }, { - title: showThinking() ? "Hide thinking" : "Show thinking", + title: showThinking() ? t("command.hide_thinking") : t("command.show_thinking"), value: "session.toggle.thinking", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setShowThinking((prev) => { const next = !prev @@ -499,19 +500,19 @@ export function Session() { }, }, { - title: "Toggle diff wrapping", + title: t("command.toggle_diff_wrapping"), value: "session.toggle.diffwrap", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setDiffWrapMode((prev) => (prev === "word" ? "none" : "word")) dialog.clear() }, }, { - title: showDetails() ? "Hide tool details" : "Show tool details", + title: showDetails() ? t("command.hide_tool_details") : t("command.show_tool_details"), value: "session.toggle.actions", keybind: "tool_details", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { const newValue = !showDetails() setShowDetails(newValue) @@ -520,10 +521,10 @@ export function Session() { }, }, { - title: "Toggle session scrollbar", + title: t("command.toggle_scrollbar"), value: "session.toggle.scrollbar", keybind: "scrollbar_toggle", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setShowScrollbar((prev) => { const next = !prev @@ -534,9 +535,9 @@ export function Session() { }, }, { - title: animationsEnabled() ? "Disable animations" : "Enable animations", + title: animationsEnabled() ? t("command.disable_animations") : t("command.enable_animations"), value: "session.toggle.animations", - category: "Session", + category: t("category.session"), onSelect: (dialog) => { setAnimationsEnabled((prev) => { const next = !prev @@ -547,10 +548,10 @@ export function Session() { }, }, { - title: "Page up", + title: t("nav.page_up"), value: "session.page.up", keybind: "messages_page_up", - category: "Session", + category: t("session.category"), disabled: true, onSelect: (dialog) => { scroll.scrollBy(-scroll.height / 2) @@ -558,10 +559,10 @@ export function Session() { }, }, { - title: "Page down", + title: t("nav.page_down"), value: "session.page.down", keybind: "messages_page_down", - category: "Session", + category: t("session.category"), disabled: true, onSelect: (dialog) => { scroll.scrollBy(scroll.height / 2) @@ -569,10 +570,10 @@ export function Session() { }, }, { - title: "Half page up", + title: t("command.half_page_up"), value: "session.half.page.up", keybind: "messages_half_page_up", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => { scroll.scrollBy(-scroll.height / 4) @@ -580,10 +581,10 @@ export function Session() { }, }, { - title: "Half page down", + title: t("command.half_page_down"), value: "session.half.page.down", keybind: "messages_half_page_down", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => { scroll.scrollBy(scroll.height / 4) @@ -591,10 +592,10 @@ export function Session() { }, }, { - title: "First message", + title: t("nav.first_message"), value: "session.first", keybind: "messages_first", - category: "Session", + category: t("session.category"), disabled: true, onSelect: (dialog) => { scroll.scrollTo(0) @@ -602,10 +603,10 @@ export function Session() { }, }, { - title: "Last message", + title: t("nav.last_message"), value: "session.last", keybind: "messages_last", - category: "Session", + category: t("session.category"), disabled: true, onSelect: (dialog) => { scroll.scrollTo(scroll.scrollHeight) @@ -613,10 +614,10 @@ export function Session() { }, }, { - title: "Jump to last user message", + title: t("command.jump_to_last_user"), value: "session.messages_last_user", keybind: "messages_last_user", - category: "Session", + category: t("category.session"), onSelect: () => { const messages = sync.data.message[route.sessionID] if (!messages || !messages.length) return @@ -644,33 +645,33 @@ export function Session() { }, }, { - title: "Next message", + title: t("command.next_message"), value: "session.message.next", keybind: "messages_next", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => scrollToMessage("next", dialog), }, { - title: "Previous message", + title: t("command.prev_message"), value: "session.message.previous", keybind: "messages_previous", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => scrollToMessage("prev", dialog), }, { - title: "Copy last assistant message", + title: t("session.copy_last"), value: "messages.copy", keybind: "messages_copy", - category: "Session", + category: t("session.category"), onSelect: (dialog) => { const revertID = session()?.revert?.messageID const lastAssistantMessage = messages().findLast( (msg) => msg.role === "assistant" && (!revertID || msg.id < revertID), ) if (!lastAssistantMessage) { - toast.show({ message: "No assistant messages found", variant: "error" }) + toast.show({ message: t("toast.no_assistant_messages"), variant: "error" }) dialog.clear() return } @@ -678,7 +679,7 @@ export function Session() { const parts = sync.data.part[lastAssistantMessage.id] ?? [] const textParts = parts.filter((part) => part.type === "text") if (textParts.length === 0) { - toast.show({ message: "No text parts found in last assistant message", variant: "error" }) + toast.show({ message: t("toast.no_text_parts"), variant: "error" }) dialog.clear() return } @@ -689,7 +690,7 @@ export function Session() { .trim() if (!text) { toast.show({ - message: "No text content found in last assistant message", + message: t("toast.no_text_content"), variant: "error", }) dialog.clear() @@ -702,16 +703,16 @@ export function Session() { /* @ts-expect-error */ renderer.writeOut(finalOsc52) Clipboard.copy(text) - .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" })) - .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) + .then(() => toast.show({ message: t("toast.message_copied"), variant: "success" })) + .catch(() => toast.show({ message: t("toast.failed_to_copy"), variant: "error" })) dialog.clear() }, }, { - title: "Copy session transcript", + title: t("command.copy_transcript"), value: "session.copy", keybind: "session_copy", - category: "Session", + category: t("category.session"), onSelect: async (dialog) => { try { const sessionData = session() @@ -726,18 +727,18 @@ export function Session() { }, ) await Clipboard.copy(transcript) - toast.show({ message: "Session transcript copied to clipboard!", variant: "success" }) + toast.show({ message: t("toast.transcript_copied"), variant: "success" }) } catch (error) { - toast.show({ message: "Failed to copy session transcript", variant: "error" }) + toast.show({ message: t("toast.failed_to_copy_transcript"), variant: "error" }) } dialog.clear() }, }, { - title: "Export session transcript", + title: t("command.export_transcript"), value: "session.export", keybind: "session_export", - category: "Session", + category: t("category.session"), onSelect: async (dialog) => { try { const sessionData = session() @@ -782,19 +783,19 @@ export function Session() { await Bun.write(filepath, result) } - toast.show({ message: `Session exported to ${filename}`, variant: "success" }) + toast.show({ message: t("toast.session_exported", { filename }), variant: "success" }) } } catch (error) { - toast.show({ message: "Failed to export session", variant: "error" }) + toast.show({ message: t("toast.failed_to_export"), variant: "error" }) } dialog.clear() }, }, { - title: "Next child session", + title: t("command.next_child_session"), value: "session.child.next", keybind: "session_child_cycle", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => { moveChild(1) @@ -802,10 +803,10 @@ export function Session() { }, }, { - title: "Previous child session", + title: t("command.prev_child_session"), value: "session.child.previous", keybind: "session_child_cycle_reverse", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => { moveChild(-1) @@ -813,10 +814,10 @@ export function Session() { }, }, { - title: "Go to parent session", + title: t("command.go_to_parent"), value: "session.parent", keybind: "session_parent", - category: "Session", + category: t("category.session"), disabled: true, onSelect: (dialog) => { const parentID = session()?.parentID @@ -936,8 +937,8 @@ export function Session() { const handleUnrevert = async () => { const confirmed = await DialogConfirm.show( dialog, - "Confirm Redo", - "Are you sure you want to restore the reverted messages?", + t("dialog.confirm_redo"), + t("dialog.confirm_redo_desc"), ) if (confirmed) { command.trigger("session.redo") @@ -1125,7 +1126,7 @@ function UserMessage(props: { - {ctx.usernameVisible() ? `${sync.data.config.username ?? "You "}` : "You "} + {ctx.usernameVisible() ? `${sync.data.config.username ?? t("ui.you") + " "}` : t("ui.you") + " "} = { } function GenericTool(props: ToolProps) { return ( - + {props.tool} {input(props.input)} ) @@ -1487,7 +1488,7 @@ function Bash(props: ToolProps) { return ( - + $ {props.input.command} {output()} @@ -1495,7 +1496,12 @@ function Bash(props: ToolProps) { - + {props.input.command} @@ -1518,7 +1524,7 @@ function Write(props: ToolProps) { return ( - + ) { {(diagnostic) => ( - Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message} + {t("tool.error_at")} [{diagnostic.range.start.line}:{diagnostic.range.start.character}]:{" "} + {diagnostic.message} )} @@ -1540,8 +1547,13 @@ function Write(props: ToolProps) { - - Write {normalizePath(props.input.filePath!)} + + {t("tool.write")} {normalizePath(props.input.filePath!)} @@ -1550,26 +1562,36 @@ function Write(props: ToolProps) { function Glob(props: ToolProps) { return ( - - Glob "{props.input.pattern}" in {normalizePath(props.input.path)} - ({props.metadata.count} matches) + + {t("tool.glob")} "{props.input.pattern}"{" "} + + {t("tool.glob_in")} {normalizePath(props.input.path)}{" "} + + + ({props.metadata.count} {t("tool.matches")}) + ) } function Read(props: ToolProps) { return ( - - Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])} + + {t("tool.read")} {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])} ) } function Grep(props: ToolProps) { return ( - - Grep "{props.input.pattern}" in {normalizePath(props.input.path)} - ({props.metadata.matches} matches) + + {t("tool.grep")} "{props.input.pattern}"{" "} + + {t("tool.glob_in")} {normalizePath(props.input.path)}{" "} + + + ({props.metadata.matches} {t("tool.matches")}) + ) } @@ -1582,16 +1604,21 @@ function List(props: ToolProps) { return "" }) return ( - - List {dir()} + + {t("tool.list")} {dir()} ) } function WebFetch(props: ToolProps) { return ( - - WebFetch {(props.input as any).url} + + {t("tool.webfetch")} {(props.input as any).url} ) } @@ -1600,8 +1627,11 @@ function CodeSearch(props: ToolProps) { const input = props.input as any const metadata = props.metadata as any return ( - - Exa Code Search "{input.query}" ({metadata.results} results) + + {t("tool.code_search")} "{input.query}"{" "} + + ({metadata.results} {t("tool.results")}) + ) } @@ -1610,8 +1640,11 @@ function WebSearch(props: ToolProps) { const input = props.input as any const metadata = props.metadata as any return ( - - Exa Web Search "{input.query}" ({metadata.numResults} results) + + {t("tool.web_search")} "{input.query}"{" "} + + ({metadata.numResults} {t("tool.results")}) + ) } @@ -1627,7 +1660,7 @@ function Task(props: ToolProps) { navigate({ type: "session", sessionID: props.metadata.sessionId! }) @@ -1637,7 +1670,7 @@ function Task(props: ToolProps) { > - {props.input.description} ({props.metadata.summary?.length} toolcalls) + {props.input.description} ({props.metadata.summary?.length} {t("tool.toolcalls")}) @@ -1648,18 +1681,18 @@ function Task(props: ToolProps) { {keybind.print("session_child_cycle")} - view subagents + {t("tool.view_subagents")} - {Locale.titlecase(props.input.subagent_type ?? "unknown")} Task "{props.input.description}" + {Locale.titlecase(props.input.subagent_type ?? "unknown")} {t("tool.task")} "{props.input.description}" @@ -1690,7 +1723,7 @@ function Edit(props: ToolProps) { return ( - + ) { {(diagnostic) => ( - Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} + {t("tool.error_at")} [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} {diagnostic.message} )} @@ -1727,8 +1760,13 @@ function Edit(props: ToolProps) { - - Edit {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })} + + {t("tool.edit")} {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })} @@ -1740,15 +1778,15 @@ function Patch(props: ToolProps) { return ( - + {props.output?.trim()} - - Patch + + {t("tool.patch")} @@ -1759,7 +1797,7 @@ function TodoWrite(props: ToolProps) { return ( - + {(todo) => } @@ -1768,8 +1806,8 @@ function TodoWrite(props: ToolProps) { - - Updating todos... + + {t("tool.pending.updating_todos")} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 095c45cef75..0d5d6f6fbe9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -12,6 +12,7 @@ import { useTextareaKeybindings } from "../../component/textarea-keybindings" import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { Locale } from "@/util/locale" +import { t } from "@/i18n" type PermissionStage = "permission" | "always" | "reject" @@ -51,7 +52,9 @@ function EditBody(props: { request: PermissionRequest }) { {"→"} - Edit {normalizePath(filepath())} + + {t("permission.edit")} {normalizePath(filepath())} + @@ -128,15 +131,15 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + - This will allow the following patterns until OpenCode is restarted + {t("permission.always_allow_patterns")} {(pattern) => ( @@ -151,7 +154,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { } - options={{ confirm: "Confirm", cancel: "Cancel" }} + options={{ confirm: t("dialog.confirm"), cancel: t("dialog.cancel") }} escapeKey="cancel" onSelect={(option) => { setStore("stage", "permission") @@ -177,23 +180,23 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + - + - + - + - + - + - + - + - + - + } - options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} + options={{ + once: t("permission.allow_once"), + always: t("permission.allow_always"), + reject: t("permission.reject"), + }} escapeKey="reject" onSelect={(option) => { if (option === "always") { @@ -285,10 +295,10 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: ( {"△"} - Reject permission + {t("permission.reject_title")} - Tell OpenCode what to do differently + {t("permission.reject_desc")} void; onCancel: ( /> - enter confirm + {t("misc.enter")} {t("misc.confirm")} - esc cancel + {t("dialog.esc")} {t("dialog.cancel")} @@ -404,10 +414,10 @@ function Prompt>(props: { - {"⇆"} select + {"⇆"} {t("permission.select")} - enter confirm + {t("misc.enter")} {t("misc.confirm")} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index a9ed042d1bb..8be1dac6389 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -11,6 +11,7 @@ import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" +import { t } from "@/i18n" export function Sidebar(props: { sessionID: string }) { const sync = useSync() @@ -90,7 +91,7 @@ export function Sidebar(props: { sessionID: string }) { - Context + {t("ui.section_context")} {context()?.tokens ?? 0} tokens {context()?.percentage ?? 0}% used @@ -107,12 +108,15 @@ export function Sidebar(props: { sessionID: string }) { {expanded.mcp ? "▼" : "▶"} - MCP + {t("ui.section_mcp")} {" "} - ({connectedMcpCount()} active - {errorMcpCount() > 0 ? `, ${errorMcpCount()} error${errorMcpCount() > 1 ? "s" : ""}` : ""}) + ({connectedMcpCount()} {t("ui.mcp_active")} + {errorMcpCount() > 0 + ? `, ${errorMcpCount()} ${errorMcpCount() > 1 ? t("ui.mcp_errors") : t("ui.mcp_error")}` + : ""} + ) @@ -141,12 +145,12 @@ export function Sidebar(props: { sessionID: string }) { {key}{" "} - Connected + {t("ui.connected")} {(val) => {val().error}} - Disabled - Needs auth + {t("ui.disabled")} + {t("ui.needs_auth")} - Needs client ID + {t("ui.needs_client_id")} @@ -167,15 +171,13 @@ export function Sidebar(props: { sessionID: string }) { {expanded.lsp ? "▼" : "▶"} - LSP + {t("ui.section_lsp")} - {sync.data.config.lsp === false - ? "LSPs have been disabled in settings" - : "LSPs will activate as files are read"} + {sync.data.config.lsp === false ? t("ui.lsp_disabled") : t("ui.lsp_will_activate")} @@ -211,7 +213,7 @@ export function Sidebar(props: { sessionID: string }) { {expanded.todo ? "▼" : "▶"} - Todo + {t("ui.section_todo")} @@ -230,7 +232,7 @@ export function Sidebar(props: { sessionID: string }) { {expanded.diff ? "▼" : "▶"} - Modified Files + {t("ui.section_modified_files")} @@ -283,18 +285,16 @@ export function Sidebar(props: { sessionID: string }) { - Getting started + {t("ui.getting_started")} kv.set("dismissed_getting_started", true)}> ✕ - OpenCode includes free models so you can start immediately. - - Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc - + {t("ui.getting_started_desc")} + {t("ui.getting_started_providers")} - Connect provider + {t("ui.connect_provider")} /connect diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7b6f028fcd9..c9d5c46b375 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -7,6 +7,7 @@ import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { t } from "@/i18n" declare global { const OPENCODE_WORKER_PATH: string @@ -58,7 +59,7 @@ export const TuiThreadCommand = cmd({ try { process.chdir(cwd) } catch (e) { - UI.error("Failed to change directory to " + cwd) + UI.error(t("thread.failed_chdir", { path: cwd })) return } diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 8431a39461d..c3db167eef2 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -4,7 +4,7 @@ import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" import { For } from "solid-js" import { useKeyboard } from "@opentui/solid" -import { Locale } from "@/util/locale" +import { t } from "@/i18n" export type DialogConfirmProps = { title: string @@ -55,9 +55,7 @@ export function DialogConfirm(props: DialogConfirmProps) { dialog.clear() }} > - - {Locale.titlecase(key)} - + {t(`dialog.${key}`)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx index 90699e1f0ba..90467a0b71e 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx @@ -4,6 +4,7 @@ import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" import { onMount, Show, type JSX } from "solid-js" import { useKeyboard } from "@opentui/solid" +import { t } from "@/i18n" export type DialogExportOptionsProps = { defaultFilename: string @@ -77,13 +78,13 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { - Export Options + {t("export.options_title")} esc - Filename: + {t("export.filename")}