From 33939f4096ebbd09163f71a18fc491f9e7833623 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 26 Jan 2026 10:22:03 +0000 Subject: [PATCH 1/9] feat(ui): add i18n scaffolding Adds a minimal i18n provider with locale preference support and migrates folder selection copy to message keys. --- packages/server/src/config/schema.ts | 1 + .../src/components/folder-selection-view.tsx | 71 +++++++----- packages/ui/src/lib/i18n/index.tsx | 104 ++++++++++++++++++ packages/ui/src/lib/i18n/messages/en.ts | 39 +++++++ packages/ui/src/main.tsx | 9 +- packages/ui/src/stores/preferences.tsx | 3 +- 6 files changed, 193 insertions(+), 34 deletions(-) create mode 100644 packages/ui/src/lib/i18n/index.tsx create mode 100644 packages/ui/src/lib/i18n/messages/en.ts diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts index 1efa49b9..d3cfeefc 100644 --- a/packages/server/src/config/schema.ts +++ b/packages/server/src/config/schema.ts @@ -13,6 +13,7 @@ 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([]), modelThinkingSelections: z.record(z.string(), z.string()).default({}), diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 14fd32eb..0ffb20d5 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -9,6 +9,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 } from "../lib/i18n" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href @@ -24,6 +25,7 @@ interface FolderSelectionViewProps { const FolderSelectionView: Component = (props) => { const { recentFolders, removeRecentFolder, preferences } = useConfig() + const { t } = useI18n() const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") @@ -181,10 +183,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 +205,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) { @@ -266,7 +268,7 @@ const FolderSelectionView: Component = (props) => {
- CodeNomad logo + {t("folderSelection.logoAlt")}

CodeNomad

@@ -275,8 +277,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 +291,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 +308,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 +320,7 @@ const FolderSelectionView: Component = (props) => {
-

Select a folder to start coding with AI

+

{t("folderSelection.tagline")}

@@ -332,16 +334,21 @@ const FolderSelectionView: Component = (props) => {
-

No Recent Folders

-

Browse for a folder to get started

+

{t("folderSelection.empty.title")}

+

{t("folderSelection.empty.description")}

} >
-

Recent Folders

+

{t("folderSelection.recent.title")}

- {folders().length} {folders().length === 1 ? "folder" : "folders"} available + {t( + folders().length === 1 + ? "folderSelection.recent.subtitle.one" + : "folderSelection.recent.subtitle.other", + { count: folders().length }, + )}

= (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 +418,8 @@ const FolderSelectionView: Component = (props) => {
@@ -424,7 +431,11 @@ const FolderSelectionView: Component = (props) => { >
- {props.isLoading ? "Opening..." : "Browse Folders"} + + {props.isLoading + ? t("folderSelection.browse.buttonOpening") + : t("folderSelection.browse.button")} +
@@ -435,7 +446,7 @@ const FolderSelectionView: Component = (props) => { @@ -457,20 +468,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 +491,8 @@ const FolderSelectionView: Component = (props) => {
-

Starting instance…

-

Hang tight while we prepare your workspace.

+

{t("folderSelection.loading.title")}

+

{t("folderSelection.loading.subtitle")}

@@ -497,8 +508,8 @@ const FolderSelectionView: Component = (props) => { setIsFolderBrowserOpen(false)} onSelect={handleBrowserSelect} /> diff --git a/packages/ui/src/lib/i18n/index.tsx b/packages/ui/src/lib/i18n/index.tsx new file mode 100644 index 00000000..c3dd7e2d --- /dev/null +++ b/packages/ui/src/lib/i18n/index.tsx @@ -0,0 +1,104 @@ +import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js" +import type { ParentComponent } from "solid-js" +import { useConfig } from "../../stores/preferences" +import { enMessages } from "./messages/en" + +type Messages = Record + +export type Locale = "en" + +const SUPPORTED_LOCALES: readonly Locale[] = ["en"] as const + +const messagesByLocale: Record = { + en: enMessages, +} + +function normalizeLocaleTag(value: string): string { + return value.trim().replace(/_/g, "-") +} + +function matchSupportedLocale(value: string | undefined): Locale | null { + if (!value) return null + + const normalized = normalizeLocaleTag(value) + const lower = normalized.toLowerCase() + const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale])) + const exact = supportedLower.get(lower) + if (exact) return exact + + const base = lower.split("-")[0] + if (!base) return null + const baseMatch = supportedLower.get(base) + return baseMatch ?? null +} + +function detectNavigatorLocale(): Locale | null { + if (typeof navigator === "undefined") return null + + const candidates = Array.isArray(navigator.languages) && navigator.languages.length > 0 + ? navigator.languages + : navigator.language + ? [navigator.language] + : [] + + for (const candidate of candidates) { + const match = matchSupportedLocale(candidate) + if (match) return match + } + + return null +} + +function interpolate(template: string, params?: Record): string { + if (!params) return template + return template.replace(/\{(\w+)\}/g, (_match, key: string) => { + const value = params[key] + return value === undefined || value === null ? "" : String(value) + }) +} + +export interface I18nContextValue { + locale: () => Locale + t: (key: string, params?: Record) => string +} + +const I18nContext = createContext() + +export const I18nProvider: ParentComponent = (props) => { + const { preferences } = useConfig() + const [detectedLocale, setDetectedLocale] = createSignal("en") + + onMount(() => { + const detected = detectNavigatorLocale() + if (detected) setDetectedLocale(detected) + }) + + const locale = createMemo(() => { + const configured = matchSupportedLocale(preferences().locale) + return configured ?? detectedLocale() ?? "en" + }) + + const messages = createMemo(() => messagesByLocale[locale()]) + + function t(key: string, params?: Record): string { + const current = messages()[key] + const fallback = enMessages[key as keyof typeof enMessages] + const template = current ?? fallback ?? key + return interpolate(template, params) + } + + const value: I18nContextValue = { + locale, + t, + } + + return {props.children} +} + +export function useI18n(): I18nContextValue { + const context = useContext(I18nContext) + if (!context) { + throw new Error("useI18n must be used within I18nProvider") + } + return context +} diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts new file mode 100644 index 00000000..0e7799db --- /dev/null +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -0,0 +1,39 @@ +export const enMessages = { + "folderSelection.logoAlt": "CodeNomad logo", + "folderSelection.tagline": "Select a folder to start coding with AI", + + "folderSelection.links.github": "CodeNomad GitHub", + "folderSelection.links.githubStars": "CodeNomad GitHub Stars", + "folderSelection.links.discord": "CodeNomad Discord", + + "folderSelection.empty.title": "No Recent Folders", + "folderSelection.empty.description": "Browse for a folder to get started", + + "folderSelection.recent.title": "Recent Folders", + "folderSelection.recent.subtitle.one": "{count} folder available", + "folderSelection.recent.subtitle.other": "{count} folders available", + "folderSelection.recent.remove": "Remove from recent", + + "folderSelection.browse.title": "Browse for Folder", + "folderSelection.browse.subtitle": "Select any folder on your computer", + "folderSelection.browse.button": "Browse Folders", + "folderSelection.browse.buttonOpening": "Opening...", + + "folderSelection.advancedSettings": "Advanced Settings", + + "folderSelection.hints.navigate": "Navigate", + "folderSelection.hints.select": "Select", + "folderSelection.hints.remove": "Remove", + "folderSelection.hints.browse": "Browse", + + "folderSelection.loading.title": "Starting instance...", + "folderSelection.loading.subtitle": "Hang tight while we prepare your workspace.", + + "folderSelection.dialog.title": "Select Workspace", + "folderSelection.dialog.description": "Select workspace to start coding.", + + "time.relative.justNow": "just now", + "time.relative.daysAgoShort": "{count}d ago", + "time.relative.hoursAgoShort": "{count}h ago", + "time.relative.minutesAgoShort": "{count}m ago", +} as const diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx index 6f74c473..16aa1866 100644 --- a/packages/ui/src/main.tsx +++ b/packages/ui/src/main.tsx @@ -4,6 +4,7 @@ import { ThemeProvider } from "./lib/theme" import { ConfigProvider } from "./stores/preferences" import { InstanceConfigProvider } from "./stores/instance-config" import { runtimeEnv } from "./lib/runtime-env" +import { I18nProvider } from "./lib/i18n" import "./index.css" import "@git-diff-view/solid/styles/diff-view-pure.css" @@ -22,9 +23,11 @@ render( () => ( - - - + + + + + ), diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 9b48b53b..6ed3377f 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -37,6 +37,7 @@ export interface Preferences { thinkingBlocksExpansion: ExpansionPreference showTimelineTools: boolean lastUsedBinary?: string + locale?: string environmentVariables: Record modelRecents: ModelPreference[] modelThinkingSelections: Record @@ -114,6 +115,7 @@ function normalizePreferences(pref?: Partial & { agentModelSelectio thinkingBlocksExpansion: sanitized.thinkingBlocksExpansion ?? defaultPreferences.thinkingBlocksExpansion, showTimelineTools: sanitized.showTimelineTools ?? defaultPreferences.showTimelineTools, lastUsedBinary: sanitized.lastUsedBinary ?? defaultPreferences.lastUsedBinary, + locale: sanitized.locale ?? defaultPreferences.locale, environmentVariables, modelRecents, modelThinkingSelections, @@ -578,4 +580,3 @@ export { recordWorkspaceLaunch, } - From 5b1e21345f92c9e80a33308767e58e5670c0bc87 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 26 Jan 2026 12:26:12 +0000 Subject: [PATCH 2/9] feat(ui): localize UI strings Converts hardcoded UI copy to i18n keys across the app, adds global translation for non-component modules, and splits the English catalog into feature modules with duplicate-key detection. --- packages/ui/src/App.tsx | 27 +-- .../components/advanced-settings-modal.tsx | 11 +- packages/ui/src/components/agent-selector.tsx | 8 +- packages/ui/src/components/alert-dialog.tsx | 31 ++- .../ui/src/components/attachment-chip.tsx | 4 +- .../background-process-output-dialog.tsx | 12 +- .../ui/src/components/code-block-inline.tsx | 6 +- .../ui/src/components/command-palette.tsx | 60 ++++-- .../components/directory-browser-dialog.tsx | 44 ++-- packages/ui/src/components/empty-state.tsx | 21 +- .../environment-variables-editor.tsx | 26 ++- packages/ui/src/components/expand-button.tsx | 5 +- .../components/filesystem-browser-dialog.tsx | 49 +++-- packages/ui/src/components/info-view.tsx | 18 +- .../instance-disconnected-modal.tsx | 19 +- packages/ui/src/components/instance-info.tsx | 26 +-- .../components/instance-service-status.tsx | 22 +- packages/ui/src/components/instance-tab.tsx | 14 +- packages/ui/src/components/instance-tabs.tsx | 10 +- .../src/components/instance-welcome-view.tsx | 56 +++--- .../components/instance/instance-shell2.tsx | 113 +++++++---- packages/ui/src/components/logs-view.tsx | 20 +- packages/ui/src/components/markdown.tsx | 10 +- packages/ui/src/components/message-block.tsx | 55 +++-- packages/ui/src/components/message-item.tsx | 60 +++--- .../ui/src/components/message-list-header.tsx | 23 ++- .../ui/src/components/message-section.tsx | 32 +-- .../ui/src/components/message-timeline.tsx | 67 +++--- packages/ui/src/components/model-selector.tsx | 8 +- .../components/opencode-binary-selector.tsx | 43 ++-- .../components/permission-approval-modal.tsx | 45 +++-- .../permission-notification-banner.tsx | 30 ++- packages/ui/src/components/prompt-input.tsx | 37 ++-- .../src/components/remote-access-overlay.tsx | 95 +++++---- packages/ui/src/components/session-list.tsx | 60 +++--- packages/ui/src/components/session-picker.tsx | 40 ++-- .../src/components/session-rename-dialog.tsx | 18 +- .../session/context-usage-panel.tsx | 18 +- .../src/components/session/session-view.tsx | 22 +- .../ui/src/components/thinking-selector.tsx | 12 +- packages/ui/src/components/tool-call.tsx | 17 +- .../tool-call/diagnostics-section.tsx | 5 +- .../src/components/tool-call/diagnostics.ts | 7 +- .../src/components/tool-call/diff-render.tsx | 11 +- .../components/tool-call/permission-block.tsx | 25 ++- .../components/tool-call/question-block.tsx | 33 +-- .../tool-call/renderers/apply-patch.tsx | 37 ++-- .../components/tool-call/renderers/bash.tsx | 5 +- .../components/tool-call/renderers/edit.tsx | 3 +- .../components/tool-call/renderers/patch.tsx | 3 +- .../tool-call/renderers/question.tsx | 10 +- .../components/tool-call/renderers/read.tsx | 7 +- .../components/tool-call/renderers/task.tsx | 36 ++-- .../components/tool-call/renderers/todo.tsx | 28 +-- .../tool-call/renderers/webfetch.tsx | 3 +- .../components/tool-call/renderers/write.tsx | 3 +- .../ui/src/components/tool-call/tool-title.ts | 18 ++ packages/ui/src/components/tool-call/types.ts | 1 + packages/ui/src/components/tool-call/utils.ts | 37 ++-- packages/ui/src/components/unified-picker.tsx | 24 ++- packages/ui/src/components/version-pill.tsx | 10 +- packages/ui/src/lib/command-utils.ts | 27 +-- packages/ui/src/lib/commands.ts | 26 ++- packages/ui/src/lib/hooks/use-commands.ts | 190 ++++++++++-------- packages/ui/src/lib/i18n/index.tsx | 43 +++- .../lib/i18n/messages/en/advancedSettings.ts | 6 + packages/ui/src/lib/i18n/messages/en/app.ts | 29 +++ .../ui/src/lib/i18n/messages/en/commands.ts | 160 +++++++++++++++ .../ui/src/lib/i18n/messages/en/dialogs.ts | 16 ++ .../ui/src/lib/i18n/messages/en/filesystem.ts | 43 ++++ .../messages/{en.ts => en/folderSelection.ts} | 7 +- packages/ui/src/lib/i18n/messages/en/index.ts | 36 ++++ .../ui/src/lib/i18n/messages/en/instance.ts | 125 ++++++++++++ .../src/lib/i18n/messages/en/loadingScreen.ts | 17 ++ packages/ui/src/lib/i18n/messages/en/logs.ts | 18 ++ .../ui/src/lib/i18n/messages/en/markdown.ts | 7 + packages/ui/src/lib/i18n/messages/en/merge.ts | 25 +++ .../ui/src/lib/i18n/messages/en/messaging.ts | 109 ++++++++++ .../src/lib/i18n/messages/en/remoteAccess.ts | 51 +++++ .../ui/src/lib/i18n/messages/en/session.ts | 67 ++++++ .../ui/src/lib/i18n/messages/en/settings.ts | 54 +++++ packages/ui/src/lib/i18n/messages/en/time.ts | 6 + .../ui/src/lib/i18n/messages/en/toolCall.ts | 121 +++++++++++ packages/ui/src/lib/markdown.ts | 18 +- packages/ui/src/renderer/loading/main.tsx | 65 +++--- packages/ui/src/stores/releases.ts | 9 +- packages/ui/src/stores/session-events.ts | 12 +- packages/ui/src/stores/session-state.ts | 15 +- 88 files changed, 2080 insertions(+), 822 deletions(-) create mode 100644 packages/ui/src/lib/i18n/messages/en/advancedSettings.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/app.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/commands.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/dialogs.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/filesystem.ts rename packages/ui/src/lib/i18n/messages/{en.ts => en/folderSelection.ts} (87%) create mode 100644 packages/ui/src/lib/i18n/messages/en/index.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/instance.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/loadingScreen.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/logs.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/markdown.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/merge.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/messaging.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/remoteAccess.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/session.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/settings.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/time.ts create mode 100644 packages/ui/src/lib/i18n/messages/en/toolCall.ts 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")}
@@ -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) =>
-

Environment Variables

-

Applied whenever a new OpenCode instance starts

+

{t("advancedSettings.environmentVariables.title")}

+

{t("advancedSettings.environmentVariables.subtitle")}

@@ -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 = () => {
- + { 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
-

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")} ) 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)

{props.title}

-

{props.description || "Search for a path under the configured workspace root."}

+

{props.description || t("filesystemBrowser.descriptionFallback")}

-

Root: {rootPath()}

+

+ {t("filesystemBrowser.rootLabel", { root: rootPath() })} +

- +
@@ -301,7 +305,11 @@ const FileSystemBrowserDialog: Component = (props) type="text" value={searchQuery()} onInput={(event) => setSearchQuery(event.currentTarget.value)} - placeholder={props.mode === "directories" ? "Search for folders" : "Search for files"} + placeholder={ + props.mode === "directories" + ? t("filesystemBrowser.search.placeholder.directories") + : t("filesystemBrowser.search.placeholder.files") + } class="selector-input" />
@@ -311,7 +319,7 @@ const FileSystemBrowserDialog: Component = (props)
-

Current folder

+

{t("filesystemBrowser.currentFolder.label")}

{currentAbsolutePath()}

@@ -336,7 +344,7 @@ const FileSystemBrowserDialog: Component = (props) >
- Loading {describeLoadingPath()}… + {t("filesystemBrowser.loading.loadingWithPath", { path: describeLoadingPath() })}
@@ -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")}

} @@ -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/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) => {
-

Server Logs

+

{t("infoView.logs.title")}

- Show server logs + {t("infoView.logs.actions.show")} } >
@@ -116,17 +118,17 @@ const InfoView: Component = (props) => { when={streamingEnabled()} fallback={
-

Server logs are paused

-

Enable streaming to watch your OpenCode server activity.

+

{t("infoView.logs.paused.title")}

+

{t("infoView.logs.paused.description")}

} > 0} - fallback={
Waiting for server output...
} + fallback={
{t("infoView.logs.empty.waiting")}
} > {(entry) => ( @@ -148,7 +150,7 @@ const InfoView: Component = (props) => { class="scroll-to-bottom" > - Scroll to bottom + {t("infoView.logs.scrollToBottom")}
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}

)}
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 (
-

Instance Information

+

{t("instanceInfo.title")}

-
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" ? ( 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) => { 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")}

@@ -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) => {
-

Resume Session

+

{t("instanceWelcome.resume.title")}

- {parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available + {parentSessions().length === 1 + ? t("instanceWelcome.resume.subtitle.one", { count: parentSessions().length }) + : t("instanceWelcome.resume.subtitle.other", { count: parentSessions().length })}

@@ -368,7 +372,7 @@ const InstanceWelcomeView: Component = (props) => { class="button-tertiary lg:hidden flex-shrink-0" onClick={openInstanceInfoOverlay} > - View Instance Info + {t("instanceWelcome.actions.viewInstanceInfo")}
@@ -404,7 +408,7 @@ const InstanceWelcomeView: Component = (props) => { "text-accent": isFocused(), }} > - {session.title || "Untitled Session"} + {session.title || t("instanceWelcome.session.untitled")}
@@ -421,7 +425,7 @@ const InstanceWelcomeView: Component = (props) => { @@ -524,7 +528,7 @@ const InstanceWelcomeView: Component = (props) => { >
@@ -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) => {
- Sessions + + {t("instanceShell.leftPanel.sessionsTitle")} +
@@ -866,8 +879,8 @@ const InstanceShell2: Component = (props) => { handleSessionSelect("info")} > @@ -876,7 +889,7 @@ const InstanceShell2: Component = (props) => { (leftPinned() ? unpinLeftDrawer() : pinLeftDrawer())} > {leftPinned() ? : } @@ -935,19 +948,19 @@ const InstanceShell2: Component = (props) => { const renderPlanSectionContent = () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") { - return

Select a session to view plan.

+ return

{t("instanceShell.plan.noSessionSelected")}

} const todoState = latestTodoState() if (!todoState) { - return

Nothing planned yet.

+ return

{t("instanceShell.plan.empty")}

} - return + return } const renderBackgroundProcesses = () => { const processes = backgroundProcessList() if (processes.length === 0) { - return

No background processes.

+ return

{t("instanceShell.backgroundProcesses.empty")}

} return ( @@ -958,9 +971,13 @@ const InstanceShell2: Component = (props) => {
{process.title}
- Status: {process.status} + {t("instanceShell.backgroundProcesses.status", { status: process.status })} - Output: {Math.round((process.outputSizeBytes ?? 0) / 1024)}KB + + {t("instanceShell.backgroundProcesses.output", { + sizeKb: Math.round((process.outputSizeBytes ?? 0) / 1024), + })} +
@@ -969,8 +986,8 @@ const InstanceShell2: Component = (props) => { type="button" class="button-tertiary w-full p-1 inline-flex items-center justify-center" onClick={() => openBackgroundOutput(process)} - aria-label="Output" - title="Output" + aria-label={t("instanceShell.backgroundProcesses.actions.output")} + title={t("instanceShell.backgroundProcesses.actions.output")} > @@ -979,8 +996,8 @@ const InstanceShell2: Component = (props) => { class="button-tertiary w-full p-1 inline-flex items-center justify-center" disabled={process.status !== "running"} onClick={() => stopBackgroundProcess(process.id)} - aria-label="Stop" - title="Stop" + aria-label={t("instanceShell.backgroundProcesses.actions.stop")} + title={t("instanceShell.backgroundProcesses.actions.stop")} > @@ -988,8 +1005,8 @@ const InstanceShell2: Component = (props) => { type="button" class="button-tertiary w-full p-1 inline-flex items-center justify-center" onClick={() => terminateBackgroundProcess(process.id)} - aria-label="Terminate" - title="Terminate" + aria-label={t("instanceShell.backgroundProcesses.actions.terminate")} + title={t("instanceShell.backgroundProcesses.actions.terminate")} > @@ -1004,17 +1021,17 @@ const InstanceShell2: Component = (props) => { const sections = [ { id: "plan", - label: "Plan", + labelKey: "instanceShell.rightPanel.sections.plan", render: renderPlanSectionContent, }, { id: "background-processes", - label: "Background Shells", + labelKey: "instanceShell.rightPanel.sections.backgroundProcesses", render: renderBackgroundProcesses, }, { id: "mcp", - label: "MCP Servers", + labelKey: "instanceShell.rightPanel.sections.mcp", render: () => ( = (props) => { }, { id: "lsp", - label: "LSP Servers", + labelKey: "instanceShell.rightPanel.sections.lsp", render: () => ( = (props) => { }, { id: "plugins", - label: "Plugins", + labelKey: "instanceShell.rightPanel.sections.plugins", render: () => ( = (props) => {
- Status Panel + {t("instanceShell.rightPanel.title")}
(rightPinned() ? unpinRightDrawer() : pinRightDrawer())} > {rightPinned() ? : } @@ -1097,7 +1114,7 @@ const InstanceShell2: Component = (props) => { > - {section.label} + {t(section.labelKey)} @@ -1274,17 +1291,17 @@ const InstanceShell2: Component = (props) => { type="button" class="connection-status-button px-2 py-0.5 text-xs" onClick={handleCommandPaletteClick} - aria-label="Open command palette" + aria-label={t("instanceShell.commandPalette.openAriaLabel")} style={{ flex: "0 0 auto", width: "auto" }} > - Command Palette + {t("instanceShell.commandPalette.button")} @@ -1307,11 +1324,15 @@ const InstanceShell2: Component = (props) => {
- Used + + {t("instanceShell.metrics.usedLabel")} + {formattedUsedTokens()}
- Avail + + {t("instanceShell.metrics.availableLabel")} + {formattedAvailableTokens()}
@@ -1333,11 +1354,15 @@ const InstanceShell2: Component = (props) => {
- Used + + {t("instanceShell.metrics.usedLabel")} + {formattedUsedTokens()}
- Avail + + {t("instanceShell.metrics.availableLabel")} + {formattedAvailableTokens()}
@@ -1353,10 +1378,10 @@ const InstanceShell2: Component = (props) => { type="button" class="connection-status-button px-2 py-0.5 text-xs" onClick={handleCommandPaletteClick} - aria-label="Open command palette" + aria-label={t("instanceShell.commandPalette.openAriaLabel")} style={{ flex: "0 0 auto", width: "auto" }} > - Command Palette + {t("instanceShell.commandPalette.button")} @@ -1371,19 +1396,19 @@ const InstanceShell2: Component = (props) => { - Connected + {t("instanceShell.connection.connected")} - Connecting... + {t("instanceShell.connection.connecting")} - Disconnected + {t("instanceShell.connection.disconnected")}
@@ -1419,8 +1444,8 @@ const InstanceShell2: Component = (props) => { fallback={
-

No session selected

-

Select a session to view messages

+

{t("instanceShell.empty.title")}

+

{t("instanceShell.empty.description")}

} diff --git a/packages/ui/src/components/logs-view.tsx b/packages/ui/src/components/logs-view.tsx index 24b89ccb..7d6e7860 100644 --- a/packages/ui/src/components/logs-view.tsx +++ b/packages/ui/src/components/logs-view.tsx @@ -1,6 +1,7 @@ import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js" import { instances, getInstanceLogs, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances" import { ChevronDown } from "lucide-solid" +import { useI18n } from "../lib/i18n" interface LogsViewProps { instanceId: string @@ -9,6 +10,7 @@ interface LogsViewProps { const logsScrollState = new Map() const LogsView: Component = (props) => { + const { t } = useI18n() let scrollRef: HTMLDivElement | undefined const savedState = logsScrollState.get(props.instanceId) const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false) @@ -83,18 +85,18 @@ const LogsView: Component = (props) => { return (
-

Server Logs

+

{t("logsView.title")}

- Show server logs + {t("logsView.actions.show")} } >
@@ -103,7 +105,7 @@ const LogsView: Component = (props) => { 0}>
- Environment Variables ({Object.keys(instance()?.environmentVariables!).length}) + {t("logsView.envVars.title", { count: Object.keys(instance()?.environmentVariables!).length })}
@@ -130,17 +132,17 @@ const LogsView: Component = (props) => { when={streamingEnabled()} fallback={
-

Server logs are paused

-

Enable streaming to watch your OpenCode server activity.

+

{t("logsView.paused.title")}

+

{t("logsView.paused.description")}

} > 0} - fallback={
Waiting for server output...
} + fallback={
{t("logsView.empty.waiting")}
} > {(entry) => ( @@ -160,7 +162,7 @@ const LogsView: Component = (props) => { class="scroll-to-bottom" > - Scroll to bottom + {t("logsView.scrollToBottom")}
diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index eeb75486..dbd22428 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -4,6 +4,7 @@ import { useGlobalCache } from "../lib/hooks/use-global-cache" import type { TextPart, RenderCache } from "../types/message" import { getLogger } from "../lib/logger" import { copyToClipboard } from "../lib/clipboard" +import { useI18n } from "../lib/i18n" const log = getLogger("session") @@ -34,6 +35,7 @@ interface MarkdownProps { } export function Markdown(props: MarkdownProps) { + const { t } = useI18n() const [html, setHtml] = createSignal("") let containerRef: HTMLDivElement | undefined let latestRequestedText = "" @@ -145,14 +147,14 @@ export function Markdown(props: MarkdownProps) { const copyText = copyButton.querySelector(".copy-text") if (copyText) { if (success) { - copyText.textContent = "Copied!" + copyText.textContent = t("markdown.codeBlock.copy.copied") setTimeout(() => { - copyText.textContent = "Copy" + copyText.textContent = t("markdown.codeBlock.copy.label") }, 2000) } else { - copyText.textContent = "Failed" + copyText.textContent = t("markdown.codeBlock.copy.failed") setTimeout(() => { - copyText.textContent = "Copy" + copyText.textContent = t("markdown.codeBlock.copy.label") }, 2000) } } diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 650df4ac..b7b47820 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -11,6 +11,7 @@ import { messageStoreBus } from "../stores/message-v2/bus" import { formatTokenTotal } from "../lib/formatters" import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" +import { useI18n } from "../lib/i18n" const TOOL_ICON = "🔧" const USER_BORDER_COLOR = "var(--message-user-border)" @@ -236,6 +237,7 @@ interface MessageBlockProps { } export default function MessageBlock(props: MessageBlockProps) { + const { t } = useI18n() const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId) @@ -465,8 +467,8 @@ export default function MessageBlock(props: MessageBlockProps) {
{TOOL_ICON} - Tool Call - {toolItem.toolPart.tool || "unknown"} + {t("messageBlock.tool.header")} + {toolItem.toolPart.tool || t("messageBlock.tool.unknown")}
@@ -538,8 +540,9 @@ interface StepCardProps { } function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; borderColor?: string }) { + const { t } = useI18n() const isAuto = () => Boolean((props.part as any)?.auto) - const label = () => (isAuto() ? "Session auto-compacted" : "Session compacted by you") + const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) const containerClass = () => @@ -550,7 +553,7 @@ function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; bo class={containerClass()} style={{ "border-left": `4px solid ${borderColor()}` }} role="status" - aria-label="Session compaction" + aria-label={t("messageBlock.compaction.ariaLabel")} >
diff --git a/packages/ui/src/components/message-list-header.tsx b/packages/ui/src/components/message-list-header.tsx index 4c48ba33..78a1d7b4 100644 --- a/packages/ui/src/components/message-list-header.tsx +++ b/packages/ui/src/components/message-list-header.tsx @@ -1,5 +1,6 @@ import { Show } from "solid-js" import Kbd from "./kbd" +import { useI18n } from "../lib/i18n" const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary" const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-primary/70" @@ -17,6 +18,7 @@ interface MessageListHeaderProps { } export default function MessageListHeader(props: MessageListHeaderProps) { + const { t } = useI18n() const hasAvailableTokens = () => typeof props.availableTokens === "number" const availableDisplay = () => (hasAvailableTokens() ? props.formatTokens(props.availableTokens as number) : "--") @@ -29,7 +31,7 @@ export default function MessageListHeader(props: MessageListHeaderProps) { type="button" class="session-sidebar-menu-button" onClick={() => props.onSidebarToggle?.()} - aria-label="Open session list" + aria-label={t("messageListHeader.sidebar.openSessionListAriaLabel")} > @@ -39,11 +41,11 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
- Used + {t("messageListHeader.metrics.usedLabel")} {props.formatTokens(props.usedTokens)}
- Avail + {t("messageListHeader.metrics.availableLabel")} {hasAvailableTokens() ? availableDisplay() : "--"}
@@ -51,8 +53,13 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
- @@ -64,19 +71,19 @@ export default function MessageListHeader(props: MessageListHeaderProps) { - Connected + {t("messageListHeader.connection.connected")} - Connecting... + {t("messageListHeader.connection.connecting")} - Disconnected + {t("messageListHeader.connection.disconnected")}
diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 6ee0f933..283e4b24 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -6,6 +6,7 @@ import { useConfig } from "../stores/preferences" import { getSessionInfo } from "../stores/sessions" import { messageStoreBus } from "../stores/message-v2/bus" import { useScrollCache } from "../lib/hooks/use-scroll-cache" +import { useI18n } from "../lib/i18n" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" const SCROLL_SCOPE = "session" @@ -31,6 +32,7 @@ export interface MessageSectionProps { export default function MessageSection(props: MessageSectionProps) { const { preferences } = useConfig() + const { t } = useI18n() const showUsagePreference = () => preferences().showUsageMetrics ?? true const showTimelineToolsPreference = () => preferences().showTimelineTools ?? true const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) @@ -107,7 +109,7 @@ export default function MessageSection(props: MessageSectionProps) { const record = resolvedStore.getMessage(messageId) if (!record) return seenTimelineMessageIds.add(messageId) - const built = buildTimelineSegments(props.instanceId, record) + const built = buildTimelineSegments(props.instanceId, record, t) built.forEach((segment) => { const key = makeTimelineKey(segment) if (seenTimelineSegmentKeys.has(key)) return @@ -121,7 +123,7 @@ export default function MessageSection(props: MessageSectionProps) { function appendTimelineForMessage(messageId: string) { const record = untrack(() => store().getMessage(messageId)) if (!record) return - const built = buildTimelineSegments(props.instanceId, record) + const built = buildTimelineSegments(props.instanceId, record, t) if (built.length === 0) return const newSegments: TimelineSegment[] = [] built.forEach((segment) => { @@ -558,7 +560,7 @@ export default function MessageSection(props: MessageSectionProps) { } previousLastTimelineMessageId = lastId previousLastTimelinePartCount = partCount - const built = buildTimelineSegments(props.instanceId, record) + const built = buildTimelineSegments(props.instanceId, record, t) const newSegments: TimelineSegment[] = [] built.forEach((segment) => { const key = makeTimelineKey(segment) @@ -753,19 +755,19 @@ export default function MessageSection(props: MessageSectionProps) {
- CodeNomad logo -

CodeNomad

+ {t("messageSection.empty.logoAlt")} +

{t("messageSection.empty.brandTitle")}

-

Start a conversation

-

Type a message below or open the Command Palette:

+

{t("messageSection.empty.title")}

+

{t("messageSection.empty.description")}

  • - Command Palette + {t("messageSection.empty.tips.commandPalette")}
  • -
  • Ask about your codebase
  • +
  • {t("messageSection.empty.tips.askAboutCodebase")}
  • - Attach files with @ + {t("messageSection.empty.tips.attachFilesPrefix")} @
@@ -775,7 +777,7 @@ export default function MessageSection(props: MessageSectionProps) {
-

Loading messages...

+

{t("messageSection.loading.messages")}

@@ -803,7 +805,7 @@ export default function MessageSection(props: MessageSectionProps) {
- @@ -812,7 +814,7 @@ export default function MessageSection(props: MessageSectionProps) { type="button" class="message-scroll-button" onClick={() => scrollToBottom(false, { suppressAutoAnchor: false })} - aria-label="Scroll to latest message" + aria-label={t("messageSection.scroll.toLatestAriaLabel")} > @@ -828,10 +830,10 @@ export default function MessageSection(props: MessageSectionProps) { >
diff --git a/packages/ui/src/components/message-timeline.tsx b/packages/ui/src/components/message-timeline.tsx index a16f4b8e..7faaca97 100644 --- a/packages/ui/src/components/message-timeline.tsx +++ b/packages/ui/src/components/message-timeline.tsx @@ -6,6 +6,7 @@ import type { MessageRecord } from "../stores/message-v2/types" import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache" import { getToolIcon } from "./tool-call/utils" import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid" +import { useI18n } from "../lib/i18n" export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction" @@ -29,14 +30,6 @@ interface MessageTimelineProps { showToolSegments?: boolean } -const SEGMENT_LABELS: Record = { - user: "You", - assistant: "Asst", - tool: "Tool", - compaction: "Compaction", -} - -const TOOL_FALLBACK_LABEL = "Tool Call" const MAX_TOOLTIP_LENGTH = 220 type ToolCallPart = Extract @@ -90,7 +83,7 @@ function collectReasoningText(part: ClientPart): string { return "" } -function collectTextFromPart(part: ClientPart): string { +function collectTextFromPart(part: ClientPart, t: (key: string, params?: Record) => string): string { if (!part) return "" if (typeof (part as any).text === "string") { return (part as any).text as string @@ -106,26 +99,28 @@ function collectTextFromPart(part: ClientPart): string { } if (part.type === "file") { const filename = (part as any)?.filename - return typeof filename === "string" && filename.length > 0 ? `[File] ${filename}` : "Attachment" + return typeof filename === "string" && filename.length > 0 + ? t("messageTimeline.text.filePrefix", { filename }) + : t("messageTimeline.text.attachment") } return "" } -function getToolTitle(part: ToolCallPart): string { +function getToolTitle(part: ToolCallPart, t: (key: string, params?: Record) => string): string { const metadata = (((part as unknown as { state?: { metadata?: unknown } })?.state?.metadata) || {}) as { title?: unknown } const title = typeof metadata.title === "string" && metadata.title.length > 0 ? metadata.title : undefined if (title) return title if (typeof part.tool === "string" && part.tool.length > 0) { return part.tool } - return TOOL_FALLBACK_LABEL + return t("messageTimeline.tool.fallbackLabel") } -function getToolTypeLabel(part: ToolCallPart): string { +function getToolTypeLabel(part: ToolCallPart, t: (key: string, params?: Record) => string): string { if (typeof part.tool === "string" && part.tool.trim().length > 0) { return part.tool.trim().slice(0, 4) } - return TOOL_FALLBACK_LABEL.slice(0, 4) + return t("messageTimeline.tool.fallbackLabel").slice(0, 4) } function formatTextsTooltip(texts: string[], fallback: string): string { @@ -139,20 +134,34 @@ function formatTextsTooltip(texts: string[], fallback: string): string { return fallback } -function formatToolTooltip(titles: string[]): string { +function formatToolTooltip( + titles: string[], + t: (key: string, params?: Record) => string, +): string { if (titles.length === 0) { - return TOOL_FALLBACK_LABEL + return t("messageTimeline.tool.fallbackLabel") } - return truncateText(`${TOOL_FALLBACK_LABEL}: ${titles.join(", ")}`) + return truncateText(`${t("messageTimeline.tool.fallbackLabel")}: ${titles.join(", ")}`) } -export function buildTimelineSegments(instanceId: string, record: MessageRecord): TimelineSegment[] { +export function buildTimelineSegments( + instanceId: string, + record: MessageRecord, + t: (key: string, params?: Record) => string, +): TimelineSegment[] { if (!record) return [] const { orderedParts } = buildRecordDisplayData(instanceId, record) if (!orderedParts || orderedParts.length === 0) { return [] } + const segmentLabel = (type: TimelineSegmentType) => { + if (type === "user") return t("messageTimeline.segment.user.label") + if (type === "assistant") return t("messageTimeline.segment.assistant.label") + if (type === "compaction") return t("messageTimeline.segment.compaction.label") + return t("messageTimeline.tool.fallbackLabel").slice(0, 4) + } + const result: TimelineSegment[] = [] let segmentIndex = 0 let pending: PendingSegment | null = null @@ -164,14 +173,14 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) } const isToolSegment = pending.type === "tool" const label = isToolSegment - ? pending.toolTypeLabels[0] || TOOL_FALLBACK_LABEL.slice(0, 4) - : SEGMENT_LABELS[pending.type] + ? pending.toolTypeLabels[0] || segmentLabel("tool") + : segmentLabel(pending.type) const shortLabel = isToolSegment ? pending.toolIcons[0] || getToolIcon("tool") : undefined const tooltip = isToolSegment - ? formatToolTooltip(pending.toolTitles) + ? formatToolTooltip(pending.toolTitles, t) : formatTextsTooltip( [...pending.texts, ...pending.reasoningTexts], - pending.type === "user" ? "User message" : "Assistant response", + pending.type === "user" ? t("messageTimeline.tooltip.userFallback") : t("messageTimeline.tooltip.assistantFallback"), ) result.push({ @@ -204,8 +213,8 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) if (part.type === "tool") { const target = ensureSegment("tool") const toolPart = part as ToolCallPart - target.toolTitles.push(getToolTitle(toolPart)) - target.toolTypeLabels.push(getToolTypeLabel(toolPart)) + target.toolTitles.push(getToolTitle(toolPart, t)) + target.toolTypeLabels.push(getToolTypeLabel(toolPart, t)) target.toolIcons.push(getToolIcon(typeof toolPart.tool === "string" ? toolPart.tool : "tool")) if (typeof toolPart.id === "string" && toolPart.id.length > 0) { target.toolPartIds.push(toolPart.id) @@ -230,8 +239,8 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) id: `${record.id}:${segmentIndex}`, messageId: record.id, type: "compaction", - label: SEGMENT_LABELS.compaction, - tooltip: isAuto ? "Auto Compaction" : "User Compaction", + label: segmentLabel("compaction"), + tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"), variant: isAuto ? "auto" : "manual", }) segmentIndex += 1 @@ -242,7 +251,7 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) continue } - const text = collectTextFromPart(part) + const text = collectTextFromPart(part, t) if (text.trim().length === 0) continue const target = ensureSegment(defaultContentType) if (target) { @@ -258,6 +267,7 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) } const MessageTimeline: Component = (props) => { + const { t } = useI18n() const buttonRefs = new Map() const store = () => messageStoreBus.getOrCreate(props.instanceId) const [hoveredSegment, setHoveredSegment] = createSignal(null) @@ -360,7 +370,7 @@ const MessageTimeline: Component = (props) => { }) return ( - }>
{(item) => { @@ -285,14 +295,17 @@ const PermissionApprovalModal: Component = (props) const showFallback = () => !resolved() - const kindLabel = () => (item.kind === "permission" ? "Permission" : "Question") + const kindLabel = () => + item.kind === "permission" + ? t("permissionApproval.kind.permission") + : t("permissionApproval.kind.question") const primaryTitle = () => { if (item.kind === "permission") { return getPermissionDisplayTitle(item.payload) } const first = item.payload.questions?.[0]?.question - return typeof first === "string" && first.trim().length > 0 ? first : "Question" + return typeof first === "string" && first.trim().length > 0 ? first : t("permissionApproval.kind.question") } const secondaryTitle = () => { @@ -300,7 +313,9 @@ const PermissionApprovalModal: Component = (props) return getPermissionKind(item.payload) } const count = item.payload.questions?.length ?? 0 - return count === 1 ? "1 question" : `${count} questions` + return count === 1 + ? t("permissionApproval.questionCount.one", { count }) + : t("permissionApproval.questionCount.other", { count }) } return ( @@ -313,7 +328,7 @@ const PermissionApprovalModal: Component = (props) {kindLabel()} {secondaryTitle()} - Active + {t("permissionApproval.status.active")}
@@ -326,7 +341,7 @@ const PermissionApprovalModal: Component = (props) handleGoToSession(sessionId()) }} > - Go to Session + {t("permissionApproval.actions.goToSession")}
@@ -360,7 +377,7 @@ const PermissionApprovalModal: Component = (props) disabled={permissionSubmitting().has(item.id)} onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "once")} > - Allow Once + {t("permissionApproval.actions.allowOnce")}
@@ -385,7 +402,7 @@ const PermissionApprovalModal: Component = (props) -
Load session for more information.
+
{t("permissionApproval.fallbackHint")}
} diff --git a/packages/ui/src/components/permission-notification-banner.tsx b/packages/ui/src/components/permission-notification-banner.tsx index 8c6f97ca..0e57ea05 100644 --- a/packages/ui/src/components/permission-notification-banner.tsx +++ b/packages/ui/src/components/permission-notification-banner.tsx @@ -1,5 +1,6 @@ import { Show, createMemo, type Component } from "solid-js" import { ShieldAlert } from "lucide-solid" +import { useI18n } from "../lib/i18n" import { getPermissionQueueLength, getQuestionQueueLength } from "../stores/instances" interface PermissionNotificationBannerProps { @@ -8,17 +9,38 @@ interface PermissionNotificationBannerProps { } const PermissionNotificationBanner: Component = (props) => { + const { t } = useI18n() const permissionCount = createMemo(() => getPermissionQueueLength(props.instanceId)) const questionCount = createMemo(() => getQuestionQueueLength(props.instanceId)) const queueLength = createMemo(() => permissionCount() + questionCount()) const hasRequests = createMemo(() => queueLength() > 0) const label = createMemo(() => { const total = queueLength() + + const pendingLabel = total === 1 + ? t("permissionBanner.pendingRequests.one", { count: total }) + : t("permissionBanner.pendingRequests.other", { count: total }) + const parts: string[] = [] - if (permissionCount() > 0) parts.push(`${permissionCount()} permission${permissionCount() === 1 ? "" : "s"}`) - if (questionCount() > 0) parts.push(`${questionCount()} question${questionCount() === 1 ? "" : "s"}`) - const detail = parts.length ? ` (${parts.join(", ")})` : "" - return `${total} pending request${total === 1 ? "" : "s"}${detail}` + + if (permissionCount() > 0) { + parts.push( + permissionCount() === 1 + ? t("permissionBanner.detail.permission.one", { count: permissionCount() }) + : t("permissionBanner.detail.permission.other", { count: permissionCount() }), + ) + } + + if (questionCount() > 0) { + parts.push( + questionCount() === 1 + ? t("permissionBanner.detail.question.one", { count: questionCount() }) + : t("permissionBanner.detail.question.other", { count: questionCount() }), + ) + } + + const detail = parts.length ? t("permissionBanner.detail.wrapper", { detail: parts.join(", ") }) : "" + return `${pendingLabel}${detail}` }) return ( diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index ab4bb636..cc1d8c7b 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -14,6 +14,7 @@ import { getActiveInstance } from "../stores/instances" import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt, executeCustomCommand } from "../stores/sessions" import { getCommands } from "../stores/commands" import { showAlertDialog } from "../stores/alerts" +import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" const log = getLogger("actions") @@ -32,6 +33,7 @@ interface PromptInputProps { } export default function PromptInput(props: PromptInputProps) { + const { t } = useI18n() const [prompt, setPromptInternal] = createSignal("") const [history, setHistory] = createSignal([]) const HISTORY_LIMIT = 100 @@ -53,9 +55,9 @@ export default function PromptInput(props: PromptInputProps) { const getPlaceholder = () => { if (mode() === "shell") { - return "Run a shell command (Esc to exit)..." + return t("promptInput.placeholder.shell") } - return "Type your message, @file, @agent, or paste images and text..." + return t("promptInput.placeholder.default") } @@ -642,8 +644,8 @@ export default function PromptInput(props: PromptInputProps) { } } catch (error) { log.error("Failed to send message:", error) - showAlertDialog("Failed to send message", { - title: "Send failed", + showAlertDialog(t("promptInput.send.errorFallback"), { + title: t("promptInput.send.errorTitle"), detail: error instanceof Error ? error.message : String(error), variant: "error", }) @@ -1048,8 +1050,11 @@ export default function PromptInput(props: PromptInputProps) { return hasText || attachments().length > 0 } - const shellHint = () => (mode() === "shell" ? { key: "Esc", text: "to exit shell mode" } : { key: "!", text: "Shell mode" }) - const commandHint = () => ({ key: "/", text: "Commands" }) + const shellHint = () => + mode() === "shell" + ? { key: "Esc", text: t("promptInput.hints.shell.exit") } + : { key: "!", text: t("promptInput.hints.shell.enable") } + const commandHint = () => ({ key: "/", text: t("promptInput.hints.commands") }) const shouldShowOverlay = () => prompt().length === 0 @@ -1115,7 +1120,7 @@ export default function PromptInput(props: PromptInputProps) { class="prompt-history-button" onClick={() => selectPreviousHistory(true)} disabled={!canHistoryGoPrevious()} - aria-label="Previous prompt" + aria-label={t("promptInput.history.previousAriaLabel")} >
@@ -196,19 +198,18 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { > - {allowExternalConnections() ? "On" : "Off"} + {allowExternalConnections() ? t("remoteAccess.toggle.on") : t("remoteAccess.toggle.off")}
- Allow connections from other IPs + {t("remoteAccess.toggle.title")} - {allowExternalConnections() ? "Binding to 0.0.0.0" : "Binding to 127.0.0.1"} + {allowExternalConnections() ? t("remoteAccess.toggle.caption.all") : t("remoteAccess.toggle.caption.local")}

- Changing this requires a restart and temporarily stops all active instances. Share the addresses below once the - server restarts. + {t("remoteAccess.toggle.note")}

@@ -217,22 +218,24 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
-

Server password

-

Remote handovers require a password. Set a memorable one to enable logins from other devices.

+

{t("remoteAccess.sections.serverPassword.label")}

+

{t("remoteAccess.sections.serverPassword.help")}

Authentication status unavailable.
} + fallback={
{t("remoteAccess.authStatus.unavailable")}
} >
-

Username: {authStatus()!.username ?? "codenomad"}

+

+ {t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })} +

{authStatus()!.passwordUserProvided - ? "A password is set for remote access." - : "No memorable password is set yet. Set one to allow remote handover logins."} + ? t("remoteAccess.password.status.set") + : t("remoteAccess.password.status.unset")}

@@ -245,26 +248,26 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { }} > {passwordFormOpen() - ? "Cancel" + ? t("remoteAccess.password.actions.cancel") : authStatus()!.passwordUserProvided - ? "Change password" - : "Set password"} + ? t("remoteAccess.password.actions.change") + : t("remoteAccess.password.actions.set")}
- + setPasswordValue(event.currentTarget.value)} - placeholder="At least 8 characters" + placeholder={t("remoteAccess.password.form.placeholder")} />
- + void handleSubmitPassword()} > - {savingPassword() ? "Saving…" : "Save password"} + {savingPassword() ? t("remoteAccess.password.save.saving") : t("remoteAccess.password.save.label")}
@@ -298,33 +301,39 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
-

Reachable addresses

-

Launch or scan from another machine to hand over control.

+

{t("remoteAccess.sections.addresses.label")}

+

{t("remoteAccess.sections.addresses.help")}

- Loading addresses…
}> + {t("remoteAccess.addresses.loading")}
}> {error()}
}> - 0} fallback={
No addresses available yet.
}> + 0} fallback={
{t("remoteAccess.addresses.none")}
}>
{(address) => { const expandedState = () => expandedUrl() === address.url const qr = () => qrCodes()[address.url] + const scopeLabel = () => + address.scope === "external" + ? t("remoteAccess.address.scope.network") + : address.scope === "loopback" + ? t("remoteAccess.address.scope.loopback") + : t("remoteAccess.address.scope.internal") return (

{address.url}

- {address.family.toUpperCase()} • {address.scope === "external" ? "Network" : address.scope === "loopback" ? "Loopback" : "Internal"} • {address.ip} + {address.family.toUpperCase()} • {scopeLabel()} • {address.ip}

diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index 31750e4b..841f50b5 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -7,6 +7,7 @@ import KeyboardHint from "./keyboard-hint" import SessionRenameDialog from "./session-rename-dialog" import { keyboardRegistry } from "../lib/keyboard-registry" import { showToastNotification } from "../lib/notifications" +import { useI18n } from "../lib/i18n" import { deleteSession, ensureSessionParentExpanded, @@ -37,17 +38,11 @@ interface SessionListProps { } function formatSessionStatus(status: SessionStatus): string { - switch (status) { - case "working": - return "Working" - case "compacting": - return "Compacting" - default: - return "Idle" - } + return status } const SessionList: Component = (props) => { + const { t } = useI18n() const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) const [isRenaming, setIsRenaming] = createSignal(false) @@ -73,13 +68,13 @@ const SessionList: Component = (props) => { try { const success = await copyToClipboard(sessionId) if (success) { - showToastNotification({ message: "Session ID copied", variant: "success" }) + showToastNotification({ message: t("sessionList.copyId.success"), variant: "success" }) } else { - showToastNotification({ message: "Unable to copy session ID", variant: "error" }) + showToastNotification({ message: t("sessionList.copyId.error"), variant: "error" }) } } catch (error) { log.error(`Failed to copy session ID ${sessionId}:`, error) - showToastNotification({ message: "Unable to copy session ID", variant: "error" }) + showToastNotification({ message: t("sessionList.copyId.error"), variant: "error" }) } } @@ -127,7 +122,7 @@ const SessionList: Component = (props) => { } } catch (error) { log.error(`Failed to delete session ${sessionId}:`, error) - showToastNotification({ message: "Unable to delete session", variant: "error" }) + showToastNotification({ message: t("sessionList.delete.error"), variant: "error" }) } } @@ -152,7 +147,7 @@ const SessionList: Component = (props) => { setRenameTarget(null) } catch (error) { log.error(`Failed to rename session ${target.id}:`, error) - showToastNotification({ message: "Unable to rename session", variant: "error" }) + showToastNotification({ message: t("sessionList.rename.error"), variant: "error" }) } finally { setIsRenaming(false) } @@ -172,14 +167,28 @@ const SessionList: Component = (props) => { return <> } const isActive = () => props.activeSessionId === rowProps.sessionId - const title = () => session()?.title || "Untitled" + const title = () => session()?.title || t("sessionList.session.untitled") const status = () => getSessionStatus(props.instanceId, rowProps.sessionId) - const statusLabel = () => formatSessionStatus(status()) + const statusLabel = () => { + switch (formatSessionStatus(status())) { + case "working": + return t("sessionList.status.working") + case "compacting": + return t("sessionList.status.compacting") + default: + return t("sessionList.status.idle") + } + } const needsPermission = () => Boolean(session()?.pendingPermission) const needsQuestion = () => Boolean((session() as any)?.pendingQuestion) const needsInput = () => needsPermission() || needsQuestion() const statusClassName = () => (needsInput() ? "session-permission" : `session-${status()}`) - const statusText = () => (needsPermission() ? "Needs Permission" : needsQuestion() ? "Needs Input" : statusLabel()) + const statusText = () => + needsPermission() + ? t("sessionList.status.needsPermission") + : needsQuestion() + ? t("sessionList.status.needsInput") + : statusLabel() return (
@@ -219,8 +228,8 @@ const SessionList: Component = (props) => { }} role="button" tabIndex={0} - aria-label={rowProps.expanded ? "Collapse session" : "Expand session"} - title={rowProps.expanded ? "Collapse" : "Expand"} + aria-label={rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")} + title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")} > @@ -240,8 +249,8 @@ const SessionList: Component = (props) => { onClick={(event) => copySessionId(event, rowProps.sessionId)} role="button" tabIndex={0} - aria-label="Copy session ID" - title="Copy session ID" + aria-label={t("sessionList.actions.copyId.ariaLabel")} + title={t("sessionList.actions.copyId.title")} > @@ -253,8 +262,8 @@ const SessionList: Component = (props) => { }} role="button" tabIndex={0} - aria-label="Rename session" - title="Rename session" + aria-label={t("sessionList.actions.rename.ariaLabel")} + title={t("sessionList.actions.rename.title")} > @@ -263,8 +272,8 @@ const SessionList: Component = (props) => { onClick={(event) => handleDeleteSession(event, rowProps.sessionId)} role="button" tabIndex={0} - aria-label="Delete session" - title="Delete session" + aria-label={t("sessionList.actions.delete.ariaLabel")} + title={t("sessionList.actions.delete.title")} > = (props) => {
{props.headerContent ?? (
-

Sessions

+

{t("sessionList.header.title")}

@@ -420,4 +429,3 @@ const SessionList: Component = (props) => { } export default SessionList - diff --git a/packages/ui/src/components/session-picker.tsx b/packages/ui/src/components/session-picker.tsx index 00f26114..b564bab2 100644 --- a/packages/ui/src/components/session-picker.tsx +++ b/packages/ui/src/components/session-picker.tsx @@ -5,6 +5,7 @@ import { getParentSessions, createSession, setActiveParentSession } from "../sto import { instances, stopInstance } from "../stores/instances" import { agents } from "../stores/sessions" import { getLogger } from "../lib/logger" +import { useI18n } from "../lib/i18n" const log = getLogger("session") @@ -15,6 +16,7 @@ interface SessionPickerProps { } const SessionPicker: Component = (props) => { + const { t } = useI18n() const [selectedAgent, setSelectedAgent] = createSignal("") const [isCreating, setIsCreating] = createSignal(false) @@ -40,10 +42,10 @@ const SessionPicker: 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") } async function handleSessionSelect(sessionId: string) { @@ -74,19 +76,19 @@ const SessionPicker: Component = (props) => {
- - - OpenCode • {instance()?.folder.split("/").pop()} - + + + {t("sessionPicker.title", { folder: instance()?.folder.split("/").pop() })} +
0} - fallback={
No previous sessions
} + fallback={
{t("sessionPicker.empty.noPrevious")}
} >

- Resume a session ({parentSessions().length}): + {t("sessionPicker.resume.title", { count: parentSessions().length })}

@@ -98,7 +100,7 @@ const SessionPicker: Component = (props) => { >
- {session.title || "Untitled"} + {session.title || t("sessionPicker.session.untitled")}
@@ -116,16 +118,16 @@ const SessionPicker: Component = (props) => {
- or + {t("sessionPicker.divider.or")}
-

Start new session:

+

{t("sessionPicker.new.title")}

0} - fallback={
Loading agents...
} + fallback={
{t("sessionPicker.agents.loading")}
} > = (props) => { type="text" value={title()} onInput={(event) => setTitle(event.currentTarget.value)} - placeholder="Enter a session name" + placeholder={t("sessionRenameDialog.input.placeholder")} class="w-full px-3 py-2 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent" />
@@ -92,7 +94,7 @@ const SessionRenameDialog: Component = (props) => { }} disabled={isSubmitting()} > - Cancel + {t("sessionRenameDialog.actions.cancel")}
diff --git a/packages/ui/src/components/session/context-usage-panel.tsx b/packages/ui/src/components/session/context-usage-panel.tsx index 7166cd8f..004709a9 100644 --- a/packages/ui/src/components/session/context-usage-panel.tsx +++ b/packages/ui/src/components/session/context-usage-panel.tsx @@ -1,6 +1,7 @@ import { createMemo, type Component } from "solid-js" import { getSessionInfo } from "../../stores/sessions" import { formatTokenTotal } from "../../lib/formatters" +import { useI18n } from "../../lib/i18n" interface ContextUsagePanelProps { instanceId: string @@ -12,6 +13,7 @@ const chipLabelClass = "uppercase text-[10px] tracking-wide text-primary/70" const headingClass = "text-xs font-semibold text-primary/70 uppercase tracking-wide" const ContextUsagePanel: Component = (props) => { + const { t } = useI18n() const info = createMemo( () => getSessionInfo(props.instanceId, props.sessionId) ?? { @@ -39,7 +41,7 @@ const ContextUsagePanel: Component = (props) => { const formatTokenValue = (value: number | null | undefined) => { - if (value === null || value === undefined) return "--" + if (value === null || value === undefined) return t("contextUsagePanel.unavailable") return formatTokenTotal(value) } @@ -48,29 +50,29 @@ const ContextUsagePanel: Component = (props) => { return (
-
Tokens
+
{t("contextUsagePanel.headings.tokens")}
- Input + {t("contextUsagePanel.labels.input")} {formatTokenTotal(inputTokens())}
- Output + {t("contextUsagePanel.labels.output")} {formatTokenTotal(outputTokens())}
- Cost + {t("contextUsagePanel.labels.cost")} {costDisplay()}
-
Context
+
{t("contextUsagePanel.headings.context")}
- Used + {t("contextUsagePanel.labels.used")} {formatTokenTotal(actualUsageTokens())}
- Avail + {t("contextUsagePanel.labels.available")} {formatTokenValue(availableTokens())}
diff --git a/packages/ui/src/components/session/session-view.tsx b/packages/ui/src/components/session/session-view.tsx index 7de204b3..12e7b92f 100644 --- a/packages/ui/src/components/session/session-view.tsx +++ b/packages/ui/src/components/session/session-view.tsx @@ -14,6 +14,7 @@ import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-stat import { showAlertDialog } from "../../stores/alerts" import { getLogger } from "../../lib/logger" import { requestData } from "../../lib/opencode-api" +import { useI18n } from "../../lib/i18n" const log = getLogger("session") @@ -34,6 +35,7 @@ interface SessionViewProps { } export const SessionView: Component = (props) => { + const { t } = useI18n() const session = () => props.activeSessions.get(props.sessionId) const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId)) const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) @@ -152,8 +154,8 @@ export const SessionView: Component = (props) => { log.info("Abort requested", { instanceId: props.instanceId, sessionId: currentSession.id }) } catch (error) { log.error("Failed to abort session", error) - showAlertDialog("Failed to stop session", { - title: "Stop failed", + showAlertDialog(t("sessionView.alerts.abortFailed.message"), { + title: t("sessionView.alerts.abortFailed.title"), detail: error instanceof Error ? error.message : String(error), variant: "error", }) @@ -201,8 +203,8 @@ export const SessionView: Component = (props) => { } } catch (error) { log.error("Failed to revert message", error) - showAlertDialog("Failed to revert to message", { - title: "Revert failed", + showAlertDialog(t("sessionView.alerts.revertFailed.message"), { + title: t("sessionView.alerts.revertFailed.title"), variant: "error", }) } @@ -237,8 +239,8 @@ export const SessionView: Component = (props) => { } } catch (error) { log.error("Failed to fork session", error) - showAlertDialog("Failed to fork session", { - title: "Fork failed", + showAlertDialog(t("sessionView.alerts.forkFailed.message"), { + title: t("sessionView.alerts.forkFailed.title"), variant: "error", }) } @@ -250,7 +252,7 @@ export const SessionView: Component = (props) => { when={session()} fallback={
-
Session not found
+
{t("sessionView.fallback.sessionNotFound")}
} > @@ -296,8 +298,8 @@ export const SessionView: Component = (props) => { type="button" class="attachment-expand" onClick={() => handleExpandTextAttachment(attachment)} - aria-label="Expand pasted text" - title="Insert pasted text" + aria-label={t("sessionView.attachments.expandPastedTextAriaLabel")} + title={t("sessionView.attachments.insertPastedTextTitle")} >