diff --git a/package-lock.json b/package-lock.json
index d361270c..aeaea7e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "codenomad-workspace",
- "version": "0.9.1",
+ "version": "0.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codenomad-workspace",
- "version": "0.9.1",
+ "version": "0.9.2",
"dependencies": {
"7zip-bin": "^5.2.0",
"google-auth-library": "^10.5.0"
@@ -7384,7 +7384,7 @@
},
"packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app",
- "version": "0.9.1",
+ "version": "0.9.2",
"dependencies": {
"@codenomad/ui": "file:../ui",
"@neuralnomads/codenomad": "file:../server"
@@ -7418,7 +7418,7 @@
},
"packages/server": {
"name": "@neuralnomads/codenomad",
- "version": "0.9.1",
+ "version": "0.9.2",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",
@@ -7455,14 +7455,14 @@
},
"packages/tauri-app": {
"name": "@codenomad/tauri-app",
- "version": "0.9.1",
+ "version": "0.9.2",
"devDependencies": {
"@tauri-apps/cli": "^2.9.4"
}
},
"packages/ui": {
"name": "@codenomad/ui",
- "version": "0.9.1",
+ "version": "0.9.2",
"dependencies": {
"@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11",
diff --git a/package.json b/package.json
index 8e3a3340..a2247d03 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "codenomad-workspace",
- "version": "0.9.1",
+ "version": "0.9.2",
"private": true,
"description": "CodeNomad monorepo workspace",
"workspaces": {
diff --git a/packages/cloudflare/release-config.json b/packages/cloudflare/release-config.json
index c2b2d832..95b1c661 100644
--- a/packages/cloudflare/release-config.json
+++ b/packages/cloudflare/release-config.json
@@ -1,4 +1,4 @@
{
- "minServerVersion": "0.9.1",
+ "minServerVersion": "0.9.2",
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
}
diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json
index cf752f00..11cae7a6 100644
--- a/packages/electron-app/package.json
+++ b/packages/electron-app/package.json
@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad-electron-app",
- "version": "0.9.1",
+ "version": "0.9.2",
"description": "CodeNomad - AI coding assistant",
"author": {
"name": "Neural Nomads",
diff --git a/packages/opencode-config/package.json b/packages/opencode-config/package.json
index a1771bb5..3ec5198a 100644
--- a/packages/opencode-config/package.json
+++ b/packages/opencode-config/package.json
@@ -3,6 +3,6 @@
"version": "0.5.0",
"private": true,
"dependencies": {
- "@opencode-ai/plugin": "1.1.30"
+ "@opencode-ai/plugin": "1.1.36"
}
}
diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json
index 65779937..79c07fbf 100644
--- a/packages/server/package-lock.json
+++ b/packages/server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@neuralnomads/codenomad",
- "version": "0.9.1",
+ "version": "0.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neuralnomads/codenomad",
- "version": "0.9.1",
+ "version": "0.9.2",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",
diff --git a/packages/server/package.json b/packages/server/package.json
index 7ee1c9fe..9d4d581b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad",
- "version": "0.9.1",
+ "version": "0.9.2",
"description": "CodeNomad Server",
"author": {
"name": "Neural Nomads",
diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts
index 1efa49b9..10b6b325 100644
--- a/packages/server/src/config/schema.ts
+++ b/packages/server/src/config/schema.ts
@@ -13,8 +13,10 @@ const PreferencesSchema = z.object({
thinkingBlocksExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
showTimelineTools: z.boolean().default(true),
lastUsedBinary: z.string().optional(),
+ locale: z.string().optional(),
environmentVariables: z.record(z.string()).default({}),
modelRecents: z.array(ModelPreferenceSchema).default([]),
+ modelFavorites: z.array(ModelPreferenceSchema).default([]),
modelThinkingSelections: z.record(z.string(), z.string()).default({}),
diffViewMode: z.enum(["split", "unified"]).default("split"),
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json
index 423e64d6..95dd9c0f 100644
--- a/packages/tauri-app/package.json
+++ b/packages/tauri-app/package.json
@@ -1,6 +1,6 @@
{
"name": "@codenomad/tauri-app",
- "version": "0.9.1",
+ "version": "0.9.2",
"private": true,
"scripts": {
"dev": "tauri dev",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 3226937a..47caede3 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@codenomad/ui",
- "version": "0.9.1",
+ "version": "0.9.2",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index 382cbe46..81064972 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -18,6 +18,7 @@ import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
import { getLogger } from "./lib/logger"
import { initReleaseNotifications } from "./stores/releases"
import { runtimeEnv } from "./lib/runtime-env"
+import { useI18n } from "./lib/i18n"
import {
hasInstances,
isSelectingFolder,
@@ -51,6 +52,7 @@ const log = getLogger("actions")
const App: Component = () => {
const { isDark } = useTheme()
+ const { t } = useI18n()
const {
preferences,
recordWorkspaceLaunch,
@@ -119,7 +121,7 @@ const App: Component = () => {
const formatLaunchErrorMessage = (error: unknown): string => {
if (!error) {
- return "Failed to launch workspace"
+ return t("app.launchError.fallbackMessage")
}
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
try {
@@ -202,12 +204,12 @@ const App: Component = () => {
async function handleCloseInstance(instanceId: string) {
const confirmed = await showConfirmDialog(
- "Stop OpenCode instance? This will stop the server.",
+ t("app.stopInstance.confirmMessage"),
{
- title: "Stop instance",
+ title: t("app.stopInstance.title"),
variant: "warning",
- confirmLabel: "Stop",
- cancelLabel: "Keep running",
+ confirmLabel: t("app.stopInstance.confirmLabel"),
+ cancelLabel: t("app.stopInstance.cancelLabel"),
},
)
@@ -330,21 +332,20 @@ const App: Component = () => {
- Unable to launch OpenCode
+ {t("app.launchError.title")}
- We couldn't start the selected OpenCode binary. Review the error output below or choose a different
- binary from Advanced Settings.
+ {t("app.launchError.description")}
-
Binary path
+
{t("app.launchError.binaryPathLabel")}
{launchErrorPath()}
-
Error output
+
{t("app.launchError.errorOutputLabel")}
{launchErrorMessage()}
@@ -356,11 +357,11 @@ const App: Component = () => {
class="selector-button selector-button-secondary"
onClick={handleLaunchErrorAdvanced}
>
- Open Advanced Settings
+ {t("app.launchError.openAdvancedSettings")}
- Close
+ {t("app.launchError.close")}
@@ -430,7 +431,7 @@ const App: Component = () => {
clearLaunchError()
}}
class="absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
- title="Close (Esc)"
+ title={t("app.launchError.closeTitle")}
>
diff --git a/packages/ui/src/components/advanced-settings-modal.tsx b/packages/ui/src/components/advanced-settings-modal.tsx
index 06a60bbe..43fac102 100644
--- a/packages/ui/src/components/advanced-settings-modal.tsx
+++ b/packages/ui/src/components/advanced-settings-modal.tsx
@@ -2,6 +2,7 @@ import { Component } from "solid-js"
import { Dialog } from "@kobalte/core/dialog"
import OpenCodeBinarySelector from "./opencode-binary-selector"
import EnvironmentVariablesEditor from "./environment-variables-editor"
+import { useI18n } from "../lib/i18n"
interface AdvancedSettingsModalProps {
open: boolean
@@ -12,6 +13,8 @@ interface AdvancedSettingsModalProps {
}
const AdvancedSettingsModal: Component = (props) => {
+ const { t } = useI18n()
+
return (
!open && props.onClose()}>
@@ -19,7 +22,7 @@ const AdvancedSettingsModal: Component = (props) =>
- Advanced Settings
+ {t("advancedSettings.title")}
@@ -32,8 +35,8 @@ const AdvancedSettingsModal: Component
= (props) =>
@@ -47,7 +50,7 @@ const AdvancedSettingsModal: Component
= (props) =>
class="selector-button selector-button-secondary"
onClick={props.onClose}
>
- Close
+ {t("advancedSettings.actions.close")}
diff --git a/packages/ui/src/components/agent-selector.tsx b/packages/ui/src/components/agent-selector.tsx
index 6541a88e..8a8c4def 100644
--- a/packages/ui/src/components/agent-selector.tsx
+++ b/packages/ui/src/components/agent-selector.tsx
@@ -3,6 +3,7 @@ import { For, Show, createEffect, createMemo } from "solid-js"
import { agents, fetchAgents, sessions } from "../stores/sessions"
import { ChevronDown } from "lucide-solid"
import type { Agent } from "../types/session"
+import { useI18n } from "../lib/i18n"
import { getLogger } from "../lib/logger"
import Kbd from "./kbd"
const log = getLogger("session")
@@ -16,6 +17,7 @@ interface AgentSelectorProps {
}
export default function AgentSelector(props: AgentSelectorProps) {
+ const { t } = useI18n()
const instanceAgents = () => agents().get(props.instanceId) || []
const session = createMemo(() => {
@@ -72,7 +74,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
options={availableAgents()}
optionValue="name"
optionTextValue="name"
- placeholder="Select agent..."
+ placeholder={t("agentSelector.placeholder")}
itemComponent={(itemProps) => (
{itemProps.item.rawValue.name}
- subagent
+ {t("agentSelector.badge.subagent")}
@@ -105,7 +107,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
{(state) => (
- Agent: {state.selectedOption()?.name ?? "None"}
+ {t("agentSelector.trigger.primary", { agent: state.selectedOption()?.name ?? t("agentSelector.none") })}
)}
diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/alert-dialog.tsx
index 413e6245..8c01082c 100644
--- a/packages/ui/src/components/alert-dialog.tsx
+++ b/packages/ui/src/components/alert-dialog.tsx
@@ -2,28 +2,26 @@ import { Dialog } from "@kobalte/core/dialog"
import { Component, Show, createEffect, createSignal } from "solid-js"
import { alertDialogState, dismissAlertDialog } from "../stores/alerts"
import type { AlertVariant, AlertDialogState } from "../stores/alerts"
+import { useI18n } from "../lib/i18n"
-const variantAccent: Record = {
+const variantAccent: Record = {
info: {
badgeBg: "var(--badge-neutral-bg)",
badgeBorder: "var(--border-base)",
badgeText: "var(--accent-primary)",
symbol: "i",
- fallbackTitle: "Heads up",
},
warning: {
badgeBg: "rgba(255, 152, 0, 0.14)",
badgeBorder: "var(--status-warning)",
badgeText: "var(--status-warning)",
symbol: "!",
- fallbackTitle: "Please review",
},
error: {
badgeBg: "var(--danger-soft-bg)",
badgeBorder: "var(--status-error)",
badgeText: "var(--status-error)",
symbol: "!",
- fallbackTitle: "Something went wrong",
},
}
@@ -60,6 +58,7 @@ function dismiss(confirmed: boolean, payload?: AlertDialogState | null, promptVa
}
const AlertDialog: Component = () => {
+ const { t } = useI18n()
let primaryButtonRef: HTMLButtonElement | undefined
let promptInputRef: HTMLInputElement | undefined
@@ -82,11 +81,25 @@ const AlertDialog: Component = () => {
{(payload) => {
const variant = payload.variant ?? "info"
const accent = variantAccent[variant]
- const title = payload.title || accent.fallbackTitle
+
+ const fallbackTitle =
+ variant === "warning"
+ ? t("alertDialog.fallbackTitle.warning")
+ : variant === "error"
+ ? t("alertDialog.fallbackTitle.error")
+ : t("alertDialog.fallbackTitle.info")
+
+ const title = payload.title || fallbackTitle
const isConfirm = payload.type === "confirm"
const isPrompt = payload.type === "prompt"
- const confirmLabel = payload.confirmLabel || (isConfirm ? "Confirm" : isPrompt ? "Run" : "OK")
- const cancelLabel = payload.cancelLabel || "Cancel"
+ const confirmLabel =
+ payload.confirmLabel ||
+ (isConfirm
+ ? t("alertDialog.actions.confirm")
+ : isPrompt
+ ? t("alertDialog.actions.run")
+ : t("alertDialog.actions.ok"))
+ const cancelLabel = payload.cancelLabel || t("alertDialog.actions.cancel")
const [inputValue, setInputValue] = createSignal(payload.inputDefaultValue ?? "")
@@ -127,7 +140,9 @@ const AlertDialog: Component = () => {
-
{payload.inputLabel || "Input"}
+
+ {payload.inputLabel || t("alertDialog.prompt.inputLabel")}
+
{
promptInputRef = el
diff --git a/packages/ui/src/components/attachment-chip.tsx b/packages/ui/src/components/attachment-chip.tsx
index 49d75817..e6e4e43a 100644
--- a/packages/ui/src/components/attachment-chip.tsx
+++ b/packages/ui/src/components/attachment-chip.tsx
@@ -1,5 +1,6 @@
import { Component } from "solid-js"
import type { Attachment } from "../types/attachment"
+import { useI18n } from "../lib/i18n"
interface AttachmentChipProps {
attachment: Attachment
@@ -7,6 +8,7 @@ interface AttachmentChipProps {
}
const AttachmentChip: Component
= (props) => {
+ const { t } = useI18n()
return (
= (props) => {
×
diff --git a/packages/ui/src/components/background-process-output-dialog.tsx b/packages/ui/src/components/background-process-output-dialog.tsx
index 89052542..64ff1e4f 100644
--- a/packages/ui/src/components/background-process-output-dialog.tsx
+++ b/packages/ui/src/components/background-process-output-dialog.tsx
@@ -3,6 +3,7 @@ import { Show, createEffect, createSignal, onCleanup } from "solid-js"
import type { BackgroundProcess } from "../../../server/src/api-types"
import { buildBackgroundProcessStreamUrl, serverApi } from "../lib/api-client"
import { createAnsiStreamRenderer, hasAnsi } from "../lib/ansi"
+import { useI18n } from "../lib/i18n"
interface BackgroundProcessOutputDialogProps {
open: boolean
@@ -12,6 +13,7 @@ interface BackgroundProcessOutputDialogProps {
}
export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDialogProps) {
+ const { t } = useI18n()
const [output, setOutput] = createSignal("")
const [outputHtml, setOutputHtml] = createSignal("")
const [ansiEnabled, setAnsiEnabled] = createSignal(false)
@@ -67,7 +69,7 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
})
.catch(() => {
if (!active) return
- setRawOutput("Failed to load output.")
+ setRawOutput(t("backgroundProcessOutputDialog.loadErrorFallback"))
setAnsiEnabled(false)
setOutputHtml("")
})
@@ -121,7 +123,7 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
- Background Output
+ {t("backgroundProcessOutputDialog.title")}
{props.process?.title} · {props.process?.id}
@@ -133,16 +135,16 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
- Close
+ {t("backgroundProcessOutputDialog.actions.close")}
- Loading output...
+ {t("backgroundProcessOutputDialog.loading")}
- Output truncated for display.
+ {t("backgroundProcessOutputDialog.truncatedNotice")}
()
@@ -15,6 +16,7 @@ interface CodeBlockInlineProps {
}
export function CodeBlockInline(props: CodeBlockInlineProps) {
+ const { t } = useI18n()
const { isDark } = useTheme()
const [html, setHtml] = createSignal("")
const [copied, setCopied] = createSignal(false)
@@ -97,8 +99,8 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
-
- Copied!
+
+ {t("codeBlockInline.actions.copied")}
diff --git a/packages/ui/src/components/command-palette.tsx b/packages/ui/src/components/command-palette.tsx
index 3d394f91..36416617 100644
--- a/packages/ui/src/components/command-palette.tsx
+++ b/packages/ui/src/components/command-palette.tsx
@@ -1,7 +1,8 @@
import { Component, createSignal, For, Show, createEffect, createMemo } from "solid-js"
import { Dialog } from "@kobalte/core/dialog"
-import type { Command } from "../lib/commands"
+import { resolveResolvable, type Command } from "../lib/commands"
import Kbd from "./kbd"
+import { useI18n } from "../lib/i18n"
interface CommandPaletteProps {
open: boolean
@@ -24,6 +25,7 @@ function buildShortcutString(shortcut: Command["shortcut"]): string {
}
const CommandPalette: Component = (props) => {
+ const { t } = useI18n()
const [query, setQuery] = createSignal("")
const [selectedCommandId, setSelectedCommandId] = createSignal(null)
const [isPointerSelecting, setIsPointerSelecting] = createSignal(false)
@@ -32,6 +34,27 @@ const CommandPalette: Component = (props) => {
const categoryOrder = ["Custom Commands", "Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"] as const
+ const categoryLabel = (category: string) => {
+ switch (category) {
+ case "Custom Commands":
+ return t("commandPalette.category.customCommands")
+ case "Instance":
+ return t("commandPalette.category.instance")
+ case "Session":
+ return t("commandPalette.category.session")
+ case "Agent & Model":
+ return t("commandPalette.category.agentModel")
+ case "Input & Focus":
+ return t("commandPalette.category.inputFocus")
+ case "System":
+ return t("commandPalette.category.system")
+ case "Other":
+ return t("commandPalette.category.other")
+ default:
+ return category
+ }
+ }
+
type CommandGroup = { category: string; commands: Command[]; startIndex: number }
type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] }
@@ -41,18 +64,21 @@ const CommandPalette: Component = (props) => {
const filtered = q
? source.filter((cmd) => {
- const label = typeof cmd.label === "function" ? cmd.label() : cmd.label
+ const label = resolveResolvable(cmd.label)
+ const description = resolveResolvable(cmd.description)
+ const keywords = cmd.keywords ? resolveResolvable(cmd.keywords) : undefined
+ const category = cmd.category ? resolveResolvable(cmd.category) : undefined
const labelMatch = label.toLowerCase().includes(q)
- const descMatch = cmd.description.toLowerCase().includes(q)
- const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q))
- const categoryMatch = cmd.category?.toLowerCase().includes(q)
+ const descMatch = description.toLowerCase().includes(q)
+ const keywordMatch = keywords?.some((k) => k.toLowerCase().includes(q))
+ const categoryMatch = category?.toLowerCase().includes(q)
return labelMatch || descMatch || keywordMatch || categoryMatch
})
: source
const groupsMap = new Map()
for (const cmd of filtered) {
- const category = cmd.category || "Other"
+ const category = (cmd.category ? resolveResolvable(cmd.category) : undefined) || "Other"
const list = groupsMap.get(category)
if (list) {
list.push(cmd)
@@ -189,12 +215,12 @@ const CommandPalette: Component = (props) => {
-
- Command Palette
- Search and execute commands
+
+ {t("commandPalette.title")}
+ {t("commandPalette.description")}
@@ -214,7 +240,7 @@ const CommandPalette: Component = (props) => {
setQuery(e.currentTarget.value)
setSelectedCommandId(null)
}}
- placeholder="Type a command or search..."
+ placeholder={t("commandPalette.searchPlaceholder")}
class="modal-search-input"
/>
@@ -228,13 +254,13 @@ const CommandPalette: Component
= (props) => {
>
0}
- fallback={No commands found for "{query()}"
}
+ fallback={{t("commandPalette.empty", { query: query() })}
}
>
{(group) => (
{(command, localIndex) => {
@@ -257,10 +283,10 @@ const CommandPalette: Component = (props) => {
>
- {typeof command.label === "function" ? command.label() : command.label}
+ {resolveResolvable(command.label)}
- {command.description}
+ {resolveResolvable(command.description)}
diff --git a/packages/ui/src/components/directory-browser-dialog.tsx b/packages/ui/src/components/directory-browser-dialog.tsx
index bab1ab8d..ca52bb29 100644
--- a/packages/ui/src/components/directory-browser-dialog.tsx
+++ b/packages/ui/src/components/directory-browser-dialog.tsx
@@ -4,6 +4,7 @@ import type { FileSystemEntry, FileSystemListingMetadata } from "../../../server
import { WINDOWS_DRIVES_ROOT } from "../../../server/src/api-types"
import { serverApi } from "../lib/api-client"
import { showAlertDialog, showPromptDialog } from "../stores/alerts"
+import { useI18n } from "../lib/i18n"
function normalizePathKey(input?: string | null) {
if (!input || input === "." || input === "./") {
@@ -62,6 +63,7 @@ type FolderRow =
| { type: "folder"; entry: FileSystemEntry }
const DirectoryBrowserDialog: Component = (props) => {
+ const { t } = useI18n()
const [rootPath, setRootPath] = createSignal("")
const [loading, setLoading] = createSignal(false)
const [error, setError] = createSignal(null)
@@ -110,7 +112,7 @@ const DirectoryBrowserDialog: Component = (props) =
const metadata = await loadDirectory()
applyMetadata(metadata)
} catch (err) {
- const message = err instanceof Error ? err.message : "Unable to load filesystem"
+ const message = err instanceof Error ? err.message : t("directoryBrowser.load.errorFallback")
setError(message)
} finally {
setLoading(false)
@@ -200,7 +202,7 @@ const DirectoryBrowserDialog: Component = (props) =
const metadata = await loadDirectory(path)
applyMetadata(metadata)
} catch (err) {
- const message = err instanceof Error ? err.message : "Unable to load filesystem"
+ const message = err instanceof Error ? err.message : t("directoryBrowser.load.errorFallback")
setError(message)
}
}
@@ -266,19 +268,19 @@ const DirectoryBrowserDialog: Component = (props) =
}
const name =
- (await showPromptDialog("Create a new folder in the current directory.", {
- title: "New Folder",
- inputLabel: "Folder name",
- inputPlaceholder: "e.g. my-new-project",
- confirmLabel: "Create",
- cancelLabel: "Cancel",
+ (await showPromptDialog(t("directoryBrowser.createFolder.promptMessage"), {
+ title: t("directoryBrowser.createFolder.title"),
+ inputLabel: t("directoryBrowser.createFolder.inputLabel"),
+ inputPlaceholder: t("directoryBrowser.createFolder.inputPlaceholder"),
+ confirmLabel: t("directoryBrowser.createFolder.confirmLabel"),
+ cancelLabel: t("directoryBrowser.createFolder.cancelLabel"),
}))?.trim() ?? ""
if (!name) return
if (name === "." || name === ".." || name.startsWith("~") || name.includes("/") || name.includes("\\")) {
- showAlertDialog("Please enter a single folder name.", {
+ showAlertDialog(t("directoryBrowser.createFolder.invalidNameMessage"), {
variant: "warning",
- detail: "Folder names cannot include slashes, '..', or '~'.",
+ detail: t("directoryBrowser.createFolder.invalidNameDetail"),
})
return
}
@@ -297,8 +299,8 @@ const DirectoryBrowserDialog: Component = (props) =
const created = await serverApi.createFileSystemFolder(metadata.currentPath, name)
await navigateTo(created.path)
} catch (err) {
- const message = err instanceof Error ? err.message : "Unable to create folder"
- showAlertDialog(message, { variant: "error", title: "Unable to create folder" })
+ const message = err instanceof Error ? err.message : t("directoryBrowser.createFolder.errorFallback")
+ showAlertDialog(message, { variant: "error", title: t("directoryBrowser.createFolder.errorFallback") })
} finally {
setCreatingFolder(false)
}
@@ -323,10 +325,10 @@ const DirectoryBrowserDialog: Component = (props) =
{props.title}
- {props.description || "Browse folders under the configured workspace root."}
+ {props.description || t("directoryBrowser.defaultDescription")}
-
+
@@ -335,7 +337,7 @@ const DirectoryBrowserDialog: Component = (props) =
- Current folder
+ {t("directoryBrowser.currentFolder")}
{currentAbsolutePath()}
@@ -350,7 +352,7 @@ const DirectoryBrowserDialog: Component = (props) =
}
}}
>
- Select Current
+ {t("directoryBrowser.selectCurrent")}
= (props) =
>
- {creatingFolder() ? "Creating…" : "New Folder"}
+ {creatingFolder() ? t("directoryBrowser.creating") : t("directoryBrowser.newFolder")}
@@ -373,7 +375,7 @@ const DirectoryBrowserDialog: Component
= (props) =
{error()}}>
- Loading folders…
+ {t("directoryBrowser.loadingFolders")}
@@ -381,13 +383,13 @@ const DirectoryBrowserDialog: Component = (props) =
>
0}
- fallback={No folders available.
}
+ fallback={{t("directoryBrowser.noFolders")}
}
>
{(item) => {
const isFolder = item.type === "folder"
- const label = isFolder ? item.entry.name || item.entry.path : "Up one level"
+ const label = isFolder ? item.entry.name || item.entry.path : t("directoryBrowser.upOneLevel")
const navigate = () => (isFolder ? handleNavigateTo(item.entry.path) : handleNavigateUp())
return (
@@ -414,7 +416,7 @@ const DirectoryBrowserDialog: Component = (props) =
handleEntrySelect(item.entry)
}}
>
- Select
+ {t("directoryBrowser.select")}
) : null}
diff --git a/packages/ui/src/components/empty-state.tsx b/packages/ui/src/components/empty-state.tsx
index c875eb97..aa1e3a32 100644
--- a/packages/ui/src/components/empty-state.tsx
+++ b/packages/ui/src/components/empty-state.tsx
@@ -1,5 +1,6 @@
import { Component } from "solid-js"
import { Loader2 } from "lucide-solid"
+import { useI18n } from "../lib/i18n"
const codeNomadIcon = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
@@ -9,15 +10,19 @@ interface EmptyStateProps {
}
const EmptyState: Component = (props) => {
+ const { t } = useI18n()
+ const modifier = typeof navigator !== "undefined" && navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"
+ const shortcut = `${modifier}+N`
+
return (
-
+
-
CodeNomad
-
Select a folder to start coding with AI
+
{t("emptyState.brandTitle")}
+
{t("emptyState.tagline")}
= (props) => {
{props.isLoading ? (
<>
- Selecting...
+ {t("emptyState.actions.selecting")}
>
) : (
- "Select Folder"
+ t("emptyState.actions.selectFolder")
)}
- Keyboard shortcut: {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+N
+ {t("emptyState.keyboardShortcut", { shortcut })}
-
Examples: ~/projects/my-app
-
You can have multiple instances of the same folder
+
{t("emptyState.examples", { example: "~/projects/my-app" })}
+
{t("emptyState.multipleInstances")}
diff --git a/packages/ui/src/components/environment-variables-editor.tsx b/packages/ui/src/components/environment-variables-editor.tsx
index c4e07b1b..07e3738d 100644
--- a/packages/ui/src/components/environment-variables-editor.tsx
+++ b/packages/ui/src/components/environment-variables-editor.tsx
@@ -1,12 +1,14 @@
import { Component, createSignal, For, Show } from "solid-js"
import { Plus, Trash2, Key, Globe } from "lucide-solid"
import { useConfig } from "../stores/preferences"
+import { useI18n } from "../lib/i18n"
interface EnvironmentVariablesEditorProps {
disabled?: boolean
}
const EnvironmentVariablesEditor: Component = (props) => {
+ const { t } = useI18n()
const {
preferences,
addEnvironmentVariable,
@@ -54,9 +56,11 @@ const EnvironmentVariablesEditor: Component = (
handleRemoveVariable(key)}
disabled={props.disabled}
class="p-1.5 icon-muted icon-danger-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
- title="Remove variable"
+ title={t("envEditor.actions.remove.title")}
>
@@ -110,7 +114,7 @@ const EnvironmentVariablesEditor: Component = (
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
- placeholder="Variable name"
+ placeholder={t("envEditor.fields.name.placeholder")}
/>
= (
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
- placeholder="Variable value"
+ placeholder={t("envEditor.fields.value.placeholder")}
/>
@@ -134,12 +138,12 @@ const EnvironmentVariablesEditor: Component = (
- No environment variables configured. Add variables above to customize the OpenCode environment.
+ {t("envEditor.empty")}
- These variables will be available in the OpenCode environment when starting instances.
+ {t("envEditor.help")}
)
diff --git a/packages/ui/src/components/expand-button.tsx b/packages/ui/src/components/expand-button.tsx
index 0b6e4fb0..953b3f87 100644
--- a/packages/ui/src/components/expand-button.tsx
+++ b/packages/ui/src/components/expand-button.tsx
@@ -1,5 +1,6 @@
import { Show } from "solid-js"
import { Maximize2, Minimize2 } from "lucide-solid"
+import { useI18n } from "../lib/i18n"
interface ExpandButtonProps {
expandState: () => "normal" | "expanded"
@@ -7,6 +8,8 @@ interface ExpandButtonProps {
}
export default function ExpandButton(props: ExpandButtonProps) {
+ const { t } = useI18n()
+
function handleClick() {
const current = props.expandState()
props.onToggleExpand(current === "normal" ? "expanded" : "normal")
@@ -17,7 +20,7 @@ export default function ExpandButton(props: ExpandButtonProps) {
type="button"
class="prompt-expand-button"
onClick={handleClick}
- aria-label="Toggle chat input height"
+ aria-label={t("expandButton.toggleAriaLabel")}
>
= (props) => {
+ const { t } = useI18n()
const [rootPath, setRootPath] = createSignal("")
const [entries, setEntries] = createSignal([])
const [currentMetadata, setCurrentMetadata] = createSignal(null)
@@ -135,7 +137,7 @@ const FileSystemBrowserDialog: Component = (props)
setRootPath(metadata.rootPath)
setEntries(directoryCache.get(normalizeEntryPath(metadata.currentPath)) ?? [])
} catch (err) {
- const message = err instanceof Error ? err.message : "Unable to load filesystem"
+ const message = err instanceof Error ? err.message : t("filesystemBrowser.errors.loadFilesystemFallback")
setError(message)
}
}
@@ -143,10 +145,10 @@ const FileSystemBrowserDialog: Component = (props)
function describeLoadingPath() {
const path = loadingPath()
if (!path) {
- return "filesystem"
+ return t("filesystemBrowser.loading.filesystem")
}
if (path === ".") {
- return rootPath() || "workspace root"
+ return rootPath() || t("filesystemBrowser.loading.workspaceRoot")
}
return resolveAbsolutePath(rootPath(), path)
}
@@ -176,7 +178,7 @@ const FileSystemBrowserDialog: Component = (props)
function handleNavigateTo(path: string) {
void fetchDirectory(path, true).catch((err) => {
log.error("Failed to open directory", err)
- setError(err instanceof Error ? err.message : "Unable to open directory")
+ setError(err instanceof Error ? err.message : t("filesystemBrowser.errors.openDirectoryFallback"))
})
}
@@ -277,19 +279,21 @@ const FileSystemBrowserDialog: Component = (props)
-
Filter
+
{t("filesystemBrowser.filterLabel")}
@@ -345,16 +353,16 @@ const FileSystemBrowserDialog: Component
= (props)
- Loading {describeLoadingPath()}…
+ {t("filesystemBrowser.loading.loadingWithPath", { path: describeLoadingPath() })}
0}
fallback={
-
No entries found.
+
{t("filesystemBrowser.empty.noEntries")}
- Retry
+ {t("filesystemBrowser.actions.retry")}
}
@@ -370,7 +378,7 @@ const FileSystemBrowserDialog: Component = (props)
- Up one level
+ {t("filesystemBrowser.navigation.upOneLevel")}
@@ -412,7 +420,7 @@ const FileSystemBrowserDialog: Component = (props)
selectEntry()
}}
>
- Select
+ {t("filesystemBrowser.actions.select")}
@@ -428,15 +436,15 @@ const FileSystemBrowserDialog: Component = (props)
↑
↓
- Navigate
+ {t("filesystemBrowser.hints.navigate")}
Enter
- Select
+ {t("filesystemBrowser.hints.select")}
Esc
- Close
+ {t("filesystemBrowser.hints.close")}
@@ -448,4 +456,3 @@ const FileSystemBrowserDialog: Component = (props)
}
export default FileSystemBrowserDialog
-
diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx
index 14fd32eb..fef0c1f3 100644
--- a/packages/ui/src/components/folder-selection-view.tsx
+++ b/packages/ui/src/components/folder-selection-view.tsx
@@ -1,5 +1,6 @@
+import { Select } from "@kobalte/core/select"
import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js"
-import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star } from "lucide-solid"
+import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown } from "lucide-solid"
import { useConfig } from "../stores/preferences"
import AdvancedSettingsModal from "./advanced-settings-modal"
import DirectoryBrowserDialog from "./directory-browser-dialog"
@@ -9,6 +10,7 @@ import VersionPill from "./version-pill"
import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons"
import { githubStars } from "../stores/github-stars"
import { formatCompactCount } from "../lib/formatters"
+import { useI18n, type Locale } from "../lib/i18n"
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
@@ -23,13 +25,27 @@ interface FolderSelectionViewProps {
}
const FolderSelectionView: Component = (props) => {
- const { recentFolders, removeRecentFolder, preferences } = useConfig()
+ const { recentFolders, removeRecentFolder, preferences, updatePreferences } = useConfig()
+ const { t, locale } = useI18n()
const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
const [isFolderBrowserOpen, setIsFolderBrowserOpen] = createSignal(false)
const nativeDialogsAvailable = supportsNativeDialogs()
let recentListRef: HTMLDivElement | undefined
+
+ type LanguageOption = { value: Locale; label: string }
+
+ const languageOptions: LanguageOption[] = [
+ { value: "en", label: "English" },
+ { value: "es", label: "Español" },
+ { value: "fr", label: "Français" },
+ { value: "ru", label: "Русский" },
+ { value: "ja", label: "日本語" },
+ { value: "zh-Hans", label: "简体中文" },
+ ]
+
+ const selectedLanguageOption = () => languageOptions.find((opt) => opt.value === locale()) ?? languageOptions[0]
const folders = () => recentFolders()
const isLoading = () => Boolean(props.isLoading)
@@ -181,10 +197,10 @@ const FolderSelectionView: Component = (props) => {
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
- if (days > 0) return `${days}d ago`
- if (hours > 0) return `${hours}h ago`
- if (minutes > 0) return `${minutes}m ago`
- return "just now"
+ if (days > 0) return t("time.relative.daysAgoShort", { count: days })
+ if (hours > 0) return t("time.relative.hoursAgoShort", { count: hours })
+ if (minutes > 0) return t("time.relative.minutesAgoShort", { count: minutes })
+ return t("time.relative.justNow")
}
function handleFolderSelect(path: string) {
@@ -203,7 +219,7 @@ const FolderSelectionView: Component = (props) => {
if (nativeDialogsAvailable) {
const fallbackPath = folders()[0]?.path
const selected = await openNativeFolderDialog({
- title: "Select Workspace",
+ title: t("folderSelection.dialog.title"),
defaultPath: fallbackPath,
})
if (selected) {
@@ -253,6 +269,50 @@ const FolderSelectionView: Component = (props) => {
class="w-full max-w-5xl h-full px-4 sm:px-8 pb-2 flex flex-col overflow-hidden"
aria-busy={isLoading() ? "true" : "false"}
>
+
+
+ value={selectedLanguageOption()}
+ onChange={(value) => {
+ if (!value) return
+ if (value.value === locale()) return
+ updatePreferences({ locale: value.value })
+ }}
+ options={languageOptions}
+ optionValue="value"
+ optionTextValue="label"
+ itemComponent={(itemProps) => (
+
+ {itemProps.item.rawValue.label}
+
+ )}
+ >
+
+
+
+ >
+ {(state) => (
+
+ {state.selectedOption()?.label}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= (props) => {
-
+
CodeNomad
@@ -275,8 +335,8 @@ const FolderSelectionView: Component = (props) => {
target="_blank"
rel="noreferrer"
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
- aria-label="CodeNomad GitHub"
- title="CodeNomad GitHub"
+ aria-label={t("folderSelection.links.github")}
+ title={t("folderSelection.links.github")}
onClick={(event) => {
event.preventDefault()
openExternalLink("https://github.com/NeuralNomadsAI/CodeNomad")
@@ -289,8 +349,8 @@ const FolderSelectionView: Component = (props) => {
target="_blank"
rel="noreferrer"
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
- aria-label="CodeNomad GitHub Stars"
- title="CodeNomad GitHub Stars"
+ aria-label={t("folderSelection.links.githubStars")}
+ title={t("folderSelection.links.githubStars")}
onClick={(event) => {
event.preventDefault()
openExternalLink("https://github.com/NeuralNomadsAI/CodeNomad")
@@ -306,8 +366,8 @@ const FolderSelectionView: Component = (props) => {
target="_blank"
rel="noreferrer"
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
- aria-label="CodeNomad Discord"
- title="CodeNomad Discord"
+ aria-label={t("folderSelection.links.discord")}
+ title={t("folderSelection.links.discord")}
onClick={(event) => {
event.preventDefault()
openExternalLink(
@@ -318,7 +378,7 @@ const FolderSelectionView: Component = (props) => {
-
Select a folder to start coding with AI
+
{t("folderSelection.tagline")}
@@ -332,16 +392,21 @@ const FolderSelectionView: Component
= (props) => {
- No Recent Folders
- Browse for a folder to get started
+ {t("folderSelection.empty.title")}
+ {t("folderSelection.empty.description")}
}
>
= (props) => {
onClick={(e) => handleRemove(folder.path, e)}
disabled={isLoading()}
class="p-2 transition-all hover:bg-red-100 dark:hover:bg-red-900/30 opacity-70 hover:opacity-100 rounded"
- title="Remove from recent"
+ title={t("folderSelection.recent.remove")}
>
@@ -411,8 +476,8 @@ const FolderSelectionView: Component
= (props) => {
@@ -424,7 +489,11 @@ const FolderSelectionView: Component
= (props) => {
>
- {props.isLoading ? "Opening..." : "Browse Folders"}
+
+ {props.isLoading
+ ? t("folderSelection.browse.buttonOpening")
+ : t("folderSelection.browse.button")}
+
@@ -435,7 +504,7 @@ const FolderSelectionView: Component = (props) => {
props.onAdvancedSettingsOpen?.()} class="panel-section-header w-full justify-between">
- Advanced Settings
+ {t("folderSelection.advancedSettings")}
@@ -457,20 +526,20 @@ const FolderSelectionView: Component = (props) => {
↑
↓
- Navigate
+ {t("folderSelection.hints.navigate")}
Enter
- Select
+ {t("folderSelection.hints.select")}
Del
- Remove
+ {t("folderSelection.hints.remove")}
- Browse
+ {t("folderSelection.hints.browse")}
@@ -480,8 +549,8 @@ const FolderSelectionView: Component
= (props) => {
-
Starting instance…
-
Hang tight while we prepare your workspace.
+
{t("folderSelection.loading.title")}
+
{t("folderSelection.loading.subtitle")}
@@ -497,8 +566,8 @@ const FolderSelectionView: Component = (props) => {
setIsFolderBrowserOpen(false)}
onSelect={handleBrowserSelect}
/>
diff --git a/packages/ui/src/components/info-view.tsx b/packages/ui/src/components/info-view.tsx
index 2310554b..4229b0ec 100644
--- a/packages/ui/src/components/info-view.tsx
+++ b/packages/ui/src/components/info-view.tsx
@@ -2,6 +2,7 @@ import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, c
import { instances, getInstanceLogs, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances"
import { ChevronDown } from "lucide-solid"
import InstanceInfo from "./instance-info"
+import { useI18n } from "../lib/i18n"
interface InfoViewProps {
instanceId: string
@@ -10,6 +11,7 @@ interface InfoViewProps {
const logsScrollState = new Map()
const InfoView: Component = (props) => {
+ const { t } = useI18n()
let scrollRef: HTMLDivElement | undefined
const savedState = logsScrollState.get(props.instanceId)
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
@@ -90,18 +92,18 @@ const InfoView: Component = (props) => {
diff --git a/packages/ui/src/components/instance-disconnected-modal.tsx b/packages/ui/src/components/instance-disconnected-modal.tsx
index af4c8c39..297bae6e 100644
--- a/packages/ui/src/components/instance-disconnected-modal.tsx
+++ b/packages/ui/src/components/instance-disconnected-modal.tsx
@@ -1,4 +1,5 @@
import { Dialog } from "@kobalte/core/dialog"
+import { useI18n } from "../lib/i18n"
interface InstanceDisconnectedModalProps {
open: boolean
@@ -8,8 +9,10 @@ interface InstanceDisconnectedModalProps {
}
export default function InstanceDisconnectedModal(props: InstanceDisconnectedModalProps) {
- const folderLabel = props.folder || "this workspace"
- const reasonLabel = props.reason || "The server stopped responding"
+ const { t } = useI18n()
+
+ const folderLabel = () => props.folder || t("instanceDisconnected.folderFallback")
+ const reasonLabel = () => props.reason || t("instanceDisconnected.reasonFallback")
return (
@@ -18,25 +21,25 @@ export default function InstanceDisconnectedModal(props: InstanceDisconnectedMod
- Instance Disconnected
+ {t("instanceDisconnected.title")}
- {folderLabel} can no longer be reached. Close the tab to continue working.
+ {t("instanceDisconnected.description", { folder: folderLabel() })}
-
Details
-
{reasonLabel}
+
{t("instanceDisconnected.details.title")}
+
{reasonLabel()}
{props.folder && (
- Folder: {props.folder}
+ {t("instanceDisconnected.details.folderLabel")} {props.folder}
)}
- Close Instance
+ {t("instanceDisconnected.actions.closeInstance")}
diff --git a/packages/ui/src/components/instance-info.tsx b/packages/ui/src/components/instance-info.tsx
index 15db4ac7..9231547a 100644
--- a/packages/ui/src/components/instance-info.tsx
+++ b/packages/ui/src/components/instance-info.tsx
@@ -2,6 +2,7 @@ import { Component, For, Show, createMemo } from "solid-js"
import type { Instance } from "../types/instance"
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
import InstanceServiceStatus from "./instance-service-status"
+import { useI18n } from "../lib/i18n"
interface InstanceInfoProps {
instance: Instance
@@ -9,6 +10,7 @@ interface InstanceInfoProps {
}
const InstanceInfo: Component
= (props) => {
+ const { t } = useI18n()
const metadataContext = useOptionalInstanceMetadataContext()
const isLoadingMetadata = metadataContext?.isLoading ?? (() => false)
const instanceAccessor = metadataContext?.instance ?? (() => props.instance)
@@ -26,11 +28,11 @@ const InstanceInfo: Component = (props) => {
return (
-
Folder
+
{t("instanceInfo.labels.folder")}
{currentInstance().folder}
@@ -41,7 +43,7 @@ const InstanceInfo: Component
= (props) => {
<>
- Project
+ {t("instanceInfo.labels.project")}
{project().id}
@@ -51,7 +53,7 @@ const InstanceInfo: Component
= (props) => {
- Version Control
+ {t("instanceInfo.labels.versionControl")}
= (props) => {
- OpenCode Version
+ {t("instanceInfo.labels.opencodeVersion")}
v{binaryVersion()}
@@ -84,7 +86,7 @@ const InstanceInfo: Component
= (props) => {
- Binary Path
+ {t("instanceInfo.labels.binaryPath")}
{currentInstance().binaryPath}
@@ -95,7 +97,7 @@ const InstanceInfo: Component
= (props) => {
0}>
- Environment Variables ({environmentEntries().length})
+ {t("instanceInfo.labels.environmentVariables", { count: environmentEntries().length })}
@@ -127,24 +129,24 @@ const InstanceInfo: Component = (props) => {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
- Loading...
+ {t("instanceInfo.loading")}
-
Server
+
{t("instanceInfo.server.title")}
- Port:
+ {t("instanceInfo.server.port")}
{currentInstance().port}
- PID:
+ {t("instanceInfo.server.pid")}
{currentInstance().pid}
-
Status:
+
{t("instanceInfo.server.status")}
= (props) => {
+ const { t } = useI18n()
const metadataContext = useOptionalInstanceMetadataContext()
const instance = metadataContext?.instance ?? (() => {
if (props.initialInstance) {
@@ -112,12 +114,12 @@ const InstanceServiceStatus: Component
= (props) =>
- LSP Servers
+ {t("instanceServiceStatus.sections.lsp")}
0}
- fallback={renderEmptyState(isLspLoading() ? "Loading LSP servers..." : "No LSP servers detected.")}
+ fallback={renderEmptyState(isLspLoading() ? t("instanceServiceStatus.lsp.loading") : t("instanceServiceStatus.lsp.empty"))}
>
@@ -132,7 +134,11 @@ const InstanceServiceStatus: Component = (props) =>
-
{server.status === "connected" ? "Connected" : "Error"}
+
+ {server.status === "connected"
+ ? t("instanceServiceStatus.lsp.status.connected")
+ : t("instanceServiceStatus.lsp.status.error")}
+
@@ -147,12 +153,12 @@ const InstanceServiceStatus: Component
= (props) =>
- MCP Servers
+ {t("instanceServiceStatus.sections.mcp")}
0}
- fallback={renderEmptyState(isMcpLoading() ? "Loading MCP servers..." : "No MCP servers detected.")}
+ fallback={renderEmptyState(isMcpLoading() ? t("instanceServiceStatus.mcp.loading") : t("instanceServiceStatus.mcp.empty"))}
>
@@ -192,7 +198,7 @@ const InstanceServiceStatus: Component = (props) =>
disabled={switchDisabled()}
color="success"
size="small"
- inputProps={{ "aria-label": `Toggle ${server.name} MCP server` }}
+ inputProps={{ "aria-label": t("instanceServiceStatus.mcp.toggleAriaLabel", { name: server.name }) }}
onChange={(_, checked) => {
if (switchDisabled()) return
void toggleMcpServer(server.name, Boolean(checked))
@@ -222,12 +228,12 @@ const InstanceServiceStatus: Component = (props) =>
- Plugins
+ {t("instanceServiceStatus.sections.plugins")}
0}
- fallback={renderEmptyState(isPluginsLoading() ? "Loading plugins..." : "No plugins configured.")}
+ fallback={renderEmptyState(isPluginsLoading() ? t("instanceServiceStatus.plugins.loading") : t("instanceServiceStatus.plugins.empty"))}
>
diff --git a/packages/ui/src/components/instance-tab.tsx b/packages/ui/src/components/instance-tab.tsx
index a53cbbb1..3fd8f6dd 100644
--- a/packages/ui/src/components/instance-tab.tsx
+++ b/packages/ui/src/components/instance-tab.tsx
@@ -2,6 +2,7 @@ import { Component, createMemo } from "solid-js"
import type { Instance } from "../types/instance"
import { getInstanceSessionIndicatorStatus } from "../stores/session-status"
import { FolderOpen, ShieldAlert, X } from "lucide-solid"
+import { useI18n } from "../lib/i18n"
interface InstanceTabProps {
instance: Instance
@@ -27,6 +28,7 @@ function formatFolderName(path: string, instances: Instance[], currentInstance:
}
const InstanceTab: Component = (props) => {
+ const { t } = useI18n()
const aggregatedStatus = createMemo(() => getInstanceSessionIndicatorStatus(props.instance.id))
const statusClassName = createMemo(() => {
const status = aggregatedStatus()
@@ -35,13 +37,13 @@ const InstanceTab: Component = (props) => {
const statusTitle = createMemo(() => {
switch (aggregatedStatus()) {
case "permission":
- return "Waiting on permission"
+ return t("instanceTab.status.permission")
case "compacting":
- return "Compacting"
+ return t("instanceTab.status.compacting")
case "working":
- return "Working"
+ return t("instanceTab.status.working")
default:
- return "Idle"
+ return t("instanceTab.status.idle")
}
})
@@ -61,7 +63,7 @@ const InstanceTab: Component = (props) => {
{aggregatedStatus() === "permission" ? (
@@ -77,7 +79,7 @@ const InstanceTab: Component = (props) => {
}}
role="button"
tabIndex={0}
- aria-label="Close instance"
+ aria-label={t("instanceTab.actions.close.ariaLabel")}
>
diff --git a/packages/ui/src/components/instance-tabs.tsx b/packages/ui/src/components/instance-tabs.tsx
index 35e7162e..2a244cbf 100644
--- a/packages/ui/src/components/instance-tabs.tsx
+++ b/packages/ui/src/components/instance-tabs.tsx
@@ -4,6 +4,7 @@ import InstanceTab from "./instance-tab"
import KeyboardHint from "./keyboard-hint"
import { Plus, MonitorUp } from "lucide-solid"
import { keyboardRegistry } from "../lib/keyboard-registry"
+import { useI18n } from "../lib/i18n"
interface InstanceTabsProps {
instances: Map
@@ -15,6 +16,7 @@ interface InstanceTabsProps {
}
const InstanceTabs: Component = (props) => {
+ const { t } = useI18n()
return (
@@ -34,8 +36,8 @@ const InstanceTabs: Component
= (props) => {
@@ -54,8 +56,8 @@ const InstanceTabs: Component = (props) => {
props.onOpenRemoteAccess?.()}
- title="Remote connect"
- aria-label="Remote connect"
+ title={t("instanceTabs.remote.title")}
+ aria-label={t("instanceTabs.remote.ariaLabel")}
>
diff --git a/packages/ui/src/components/instance-welcome-view.tsx b/packages/ui/src/components/instance-welcome-view.tsx
index 15ca0365..38622aed 100644
--- a/packages/ui/src/components/instance-welcome-view.tsx
+++ b/packages/ui/src/components/instance-welcome-view.tsx
@@ -9,6 +9,7 @@ import SessionRenameDialog from "./session-rename-dialog"
import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry"
import { isMac } from "../lib/keyboard-utils"
import { showToastNotification } from "../lib/notifications"
+import { useI18n } from "../lib/i18n"
import { getLogger } from "../lib/logger"
const log = getLogger("actions")
@@ -19,6 +20,7 @@ interface InstanceWelcomeViewProps {
}
const InstanceWelcomeView: Component = (props) => {
+ const { t } = useI18n()
const [isCreating, setIsCreating] = createSignal(false)
const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions")
@@ -47,7 +49,7 @@ const InstanceWelcomeView: Component = (props) => {
ctrl: !isMac(),
},
handler: () => {},
- description: "New Session",
+ description: t("instanceWelcome.shortcuts.newSession"),
context: "global",
}
})
@@ -248,10 +250,10 @@ const InstanceWelcomeView: Component = (props) => {
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
- if (days > 0) return `${days}d ago`
- if (hours > 0) return `${hours}h ago`
- if (minutes > 0) return `${minutes}m ago`
- return "just now"
+ if (days > 0) return t("time.relative.daysAgoShort", { count: days })
+ if (hours > 0) return t("time.relative.hoursAgoShort", { count: hours })
+ if (minutes > 0) return t("time.relative.minutesAgoShort", { count: minutes })
+ return t("time.relative.justNow")
}
function formatTimestamp(timestamp: number): string {
@@ -291,7 +293,7 @@ const InstanceWelcomeView: Component = (props) => {
setRenameTarget(null)
} catch (error) {
log.error("Failed to rename session:", error)
- showToastNotification({ message: "Unable to rename session", variant: "error" })
+ showToastNotification({ message: t("instanceWelcome.toasts.renameError"), variant: "error" })
} finally {
setIsRenaming(false)
}
@@ -333,11 +335,11 @@ const InstanceWelcomeView: Component = (props) => {
/>
-
No Previous Sessions
-
Create a new session below to get started
+
{t("instanceWelcome.empty.title")}
+
{t("instanceWelcome.empty.description")}
- View Instance Info
+ {t("instanceWelcome.actions.viewInstanceInfo")}
@@ -347,8 +349,8 @@ const InstanceWelcomeView: Component = (props) => {
- Loading Sessions
- Fetching your previous sessions...
+ {t("instanceWelcome.loading.title")}
+ {t("instanceWelcome.loading.description")}
}
@@ -357,9 +359,11 @@ const InstanceWelcomeView: Component = (props) => {
@@ -421,7 +425,7 @@ const InstanceWelcomeView: Component
= (props) => {
{
event.preventDefault()
event.stopPropagation()
@@ -433,7 +437,7 @@ const InstanceWelcomeView: Component = (props) => {
{
event.preventDefault()
@@ -470,8 +474,8 @@ const InstanceWelcomeView: Component = (props) => {
@@ -496,7 +500,7 @@ const InstanceWelcomeView: Component
= (props) => {
)}
- Create Session
+ {t("instanceWelcome.new.createButton")}
@@ -524,7 +528,7 @@ const InstanceWelcomeView: Component
= (props) => {
>
- Close
+ {t("instanceWelcome.overlay.close")}
@@ -541,25 +545,25 @@ const InstanceWelcomeView: Component
= (props) => {
↑
↓
- Navigate
+ {t("instanceWelcome.hints.navigate")}
PgUp
PgDn
- Jump
+ {t("instanceWelcome.hints.jump")}
Home
End
- First/Last
+ {t("instanceWelcome.hints.firstLast")}
Enter
- Resume
+ {t("instanceWelcome.hints.resume")}
Del
- Delete
+ {t("instanceWelcome.hints.delete")}
diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx
index 15753435..dedcd12e 100644
--- a/packages/ui/src/components/instance/instance-shell2.tsx
+++ b/packages/ui/src/components/instance/instance-shell2.tsx
@@ -67,6 +67,7 @@ import { getLogger } from "../../lib/logger"
import { serverApi } from "../../lib/api-client"
import { getBackgroundProcesses, loadBackgroundProcesses } from "../../stores/background-processes"
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
+import { useI18n } from "../../lib/i18n"
import {
SESSION_SIDEBAR_EVENT,
type SessionSidebarRequestAction,
@@ -121,6 +122,8 @@ function persistPinState(side: "left" | "right", value: boolean) {
}
const InstanceShell2: Component
= (props) => {
+ const { t } = useI18n()
+
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH)
const [leftPinned, setLeftPinned] = createSignal(true)
@@ -357,6 +360,14 @@ const InstanceShell2: Component = (props) => {
return "disconnected"
}
+ const connectionStatusLabel = () => {
+ const status = connectionStatus()
+ if (status === "connected") return t("instanceShell.connection.connected")
+ if (status === "connecting") return t("instanceShell.connection.connecting")
+ if (status === "error" || status === "disconnected") return t("instanceShell.connection.disconnected")
+ return t("instanceShell.connection.unknown")
+ }
+
const handleCommandPaletteClick = () => {
showCommandPalette(props.instance.id)
}
@@ -716,16 +727,16 @@ const InstanceShell2: Component = (props) => {
const leftAppBarButtonLabel = () => {
const state = leftDrawerState()
- if (state === "pinned") return "Left drawer pinned"
- if (state === "floating-closed") return "Open left drawer"
- return "Close left drawer"
+ if (state === "pinned") return t("instanceShell.leftDrawer.toggle.pinned")
+ if (state === "floating-closed") return t("instanceShell.leftDrawer.toggle.open")
+ return t("instanceShell.leftDrawer.toggle.close")
}
const rightAppBarButtonLabel = () => {
const state = rightDrawerState()
- if (state === "pinned") return "Right drawer pinned"
- if (state === "floating-closed") return "Open right drawer"
- return "Close right drawer"
+ if (state === "pinned") return t("instanceShell.rightDrawer.toggle.pinned")
+ if (state === "floating-closed") return t("instanceShell.rightDrawer.toggle.open")
+ return t("instanceShell.rightDrawer.toggle.close")
}
const leftAppBarButtonIcon = () => {
@@ -855,7 +866,9 @@ const InstanceShell2: Component = (props) => {