diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 2a9580af3fdc..5babe8d94cee 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -17,15 +17,12 @@ import { hermesDirectiveFormatter } from '@/components/assistant-ui/directive-te import { Button } from '@/components/ui/button' import { useMediaQuery } from '@/hooks/use-media-query' import { useResizeObserver } from '@/hooks/use-resize-observer' +import { useDesktopI18n } from '@/i18n' import { chatMessageText } from '@/lib/chat-messages' import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images' import { triggerHaptic } from '@/lib/haptics' import { cn } from '@/lib/utils' -import { - $composerAttachments, - clearComposerAttachments, - type ComposerAttachment -} from '@/store/composer' +import { $composerAttachments, clearComposerAttachments, type ComposerAttachment } from '@/store/composer' import { $queuedPromptsBySession, enqueueQueuedPrompt, @@ -106,6 +103,7 @@ export function ChatBar({ onSubmit, onTranscribeAudio }: ChatBarProps) { + const { t } = useDesktopI18n() const aui = useAui() const draft = useAuiState(s => s.composer.text) const attachments = useStore($composerAttachments) @@ -149,7 +147,7 @@ export function ChatBar({ const busyAction = busy && hasComposerPayload ? 'queue' : 'stop' const showHelpHint = draft === '?' - const placeholder = disabled ? 'Starting Hermes...' : 'Send follow-up' + const placeholder = disabled ? t('chat.startingHermes') : t('chat.sendFollowUp') const focusInput = useCallback(() => { focusComposerInput(editorRef.current) @@ -1023,9 +1021,9 @@ export function ChatBar({ const input = (
{queueEdit && editingQueuedPrompt && (
-
- Editing queued turn in composer -
+
{t('chat.editingQueuedTurn')}
diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index c9e0bb0db449..d87c6cea7269 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -16,6 +16,7 @@ import { NotificationStack } from '@/components/notifications' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { getGlobalModelOptions, type HermesGateway } from '@/hermes' +import { useDesktopI18n } from '@/i18n' import type { ChatMessage } from '@/lib/chat-messages' import { quickModelOptions, sessionTitle, toRuntimeMessage } from '@/lib/chat-runtime' import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime' @@ -94,10 +95,11 @@ function ChatHeader({ onToggleSelectedPin, selectedSessionId }: ChatHeaderProps) { + const { t } = useDesktopI18n() const sessions = useStore($sessions) const pinnedSessionIds = useStore($pinnedSessionIds) const activeStoredSession = sessions.find(session => session.id === selectedSessionId) || null - const title = activeStoredSession ? sessionTitle(activeStoredSession) : 'New session' + const title = activeStoredSession ? sessionTitle(activeStoredSession) : t('chat.newSession') const selectedIsPinned = selectedSessionId ? pinnedSessionIds.includes(selectedSessionId) : false return ( diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index d02c0e8e9555..4b13512adf4a 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -34,6 +34,7 @@ import { } from '@/components/ui/sidebar' import { Skeleton } from '@/components/ui/skeleton' import { searchSessions, type SessionInfo, type SessionSearchResult } from '@/hermes' +import { useDesktopI18n } from '@/i18n' import { cn } from '@/lib/utils' import { $pinnedSessionIds, @@ -157,13 +158,13 @@ function searchResultToSession(result: SessionSearchResult): SessionInfo { } } -function workspaceGroupsFor(sessions: SessionInfo[]): SidebarSessionGroup[] { +function workspaceGroupsFor(sessions: SessionInfo[], noWorkspaceLabel: string): SidebarSessionGroup[] { const groups = new Map() for (const session of sessions) { const path = session.cwd?.trim() || '' const id = path || '__no_workspace__' - const label = baseName(path) || path || 'No workspace' + const label = baseName(path) || path || noWorkspaceLabel const group = groups.get(id) ?? { id, label, path: path || null, sessions: [] } group.sessions.push(session) @@ -212,6 +213,7 @@ export function ChatSidebar({ onArchiveSession, onNewSessionInWorkspace }: ChatSidebarProps) { + const { t } = useDesktopI18n() const sidebarOpen = useStore($sidebarOpen) const agentsGrouped = useStore($sidebarAgentsGrouped) const pinnedSessionIds = useStore($pinnedSessionIds) @@ -338,8 +340,8 @@ export function ChatSidebar({ ) const agentGroups = useMemo( - () => orderByIds(workspaceGroupsFor(agentSessions), g => g.id, workspaceOrderIds), - [agentSessions, workspaceOrderIds] + () => orderByIds(workspaceGroupsFor(agentSessions, t('sidebar.noWorkspace')), g => g.id, workspaceOrderIds), + [agentSessions, workspaceOrderIds, t] ) const showSessionSkeletons = sessionsLoading && sortedSessions.length === 0 @@ -418,6 +420,7 @@ export function ChatSidebar({ {SIDEBAR_NAV.map(item => { const isInteractive = Boolean(item.action) || Boolean(item.route) + const label = t(`nav.${item.id === 'new-session' ? 'newSession' : item.id}`) const active = (item.id === 'skills' && currentView === 'skills') || @@ -435,14 +438,14 @@ export function ChatSidebar({ !isInteractive && 'cursor-default hover:border-transparent hover:bg-transparent hover:text-inherit' )} - onClick={() => onNavigate(item)} - tooltip={item.label} + onClick={() => onNavigate({ ...item, label })} + tooltip={label} type="button" > {sidebarOpen && ( <> - {item.label} + {label} {item.id === 'new-session' && ( )} @@ -461,16 +464,16 @@ export function ChatSidebar({
setSearchQuery(event.target.value)} - placeholder="Search sessions…" + placeholder={t('sidebar.searchPlaceholder')} type="text" value={searchQuery} /> {searchQuery && ( ) : null } - label="Sessions" + label={t('sidebar.sessions')} labelMeta={countLabel(agentSessions.length, knownSessionTotal)} onArchiveSession={onArchiveSession} onDeleteSession={onDeleteSession} @@ -632,19 +635,25 @@ function SidebarSessionSkeletons() { ) } -const SidebarAllPinnedState = () => ( -
- Everything here is pinned. Unpin a chat to show it in recents. -
-) +const SidebarAllPinnedState = () => { + const { t } = useDesktopI18n() + + return ( +
+ {t('sidebar.allPinned')} +
+ ) +} function SidebarPinnedEmptyState() { + const { t } = useDesktopI18n() + return (
- Shift-click a chat to pin · drag to reorder + {t('sidebar.pinHint')}
) } @@ -837,6 +846,7 @@ function SidebarWorkspaceGroup({ ref, ...rest }: SidebarWorkspaceGroupProps) { + const { t } = useDesktopI18n() const [open, setOpen] = useState(true) const [visibleCount, setVisibleCount] = useState(WORKSPACE_PAGE) const visibleSessions = group.sessions.slice(0, visibleCount) @@ -861,10 +871,10 @@ function SidebarWorkspaceGroup({ {onNewSession && ( diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index 81359a6f8541..ac59805cdb5b 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -3,6 +3,7 @@ import type * as React from 'react' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import type { SessionInfo } from '@/hermes' +import { useDesktopI18n } from '@/i18n' import { sessionTitle } from '@/lib/chat-runtime' import { triggerHaptic } from '@/lib/haptics' import { cn } from '@/lib/utils' @@ -58,9 +59,10 @@ export function SidebarSessionRow({ ref, ...rest }: SidebarSessionRowProps) { + const { t } = useDesktopI18n() const title = sessionTitle(session) const age = formatAge(session.last_active || session.started_at) - const handleLabel = `Reorder ${title}` + const handleLabel = t('session.reorder', { title }) return ( + ) + })} +
+ + +
+
+
+
{t('appearance.colorMode.title')}
+
{t('appearance.colorMode.description')}
{prettyName(mode)}
- {MODE_OPTIONS.map(({ id, label, description, icon: Icon }) => { + {modeOptions.map(({ id, label, description, icon: Icon }) => { const active = mode === id return ( @@ -117,25 +198,25 @@ export function AppearanceSettings() {
-
Tool Call Display
-
- Product hides raw tool payloads; Technical shows full input/output. -
+
{t('appearance.toolView.title')}
+
{t('appearance.toolView.description')}
- {toolViewMode === 'technical' ? 'Technical' : 'Product'} + + {toolViewMode === 'technical' ? t('appearance.toolView.technical') : t('appearance.toolView.product')} +
{( [ { id: 'product', - label: 'Product', - description: 'Human-friendly tool activity with concise summaries.' + label: t('appearance.toolView.product'), + description: t('appearance.toolView.productDescription') }, { id: 'technical', - label: 'Technical', - description: 'Include raw tool args/results and low-level details.' + label: t('appearance.toolView.technical'), + description: t('appearance.toolView.technicalDescription') } ] as const ).map(option => { @@ -174,10 +255,8 @@ export function AppearanceSettings() {
-
Theme
-
- Desktop palettes only. The selected mode is applied on top. -
+
{t('appearance.theme.title')}
+
{t('appearance.theme.description')}
{activeTheme && {activeTheme.label}}
@@ -223,3 +302,9 @@ export function AppearanceSettings() { ) } + +const BUILTIN_MODE_ICONS = { + dark: Moon, + light: Sun, + system: Monitor +} diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx index 3969c2c09b51..01c7cf9a83c0 100644 --- a/apps/desktop/src/app/settings/config-settings.tsx +++ b/apps/desktop/src/app/settings/config-settings.tsx @@ -12,6 +12,7 @@ import { getHermesConfigSchema, saveHermesConfig } from '@/hermes' +import { useDesktopI18n } from '@/i18n' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' import type { ConfigFieldSchema, HermesConfigRecord } from '@/types/hermes' @@ -37,7 +38,15 @@ function ConfigField({ optionLabels?: Record onChange: (value: unknown) => void }) { - const label = FIELD_LABELS[schemaKey] ?? prettyName(schemaKey.split('.').pop() ?? schemaKey) + const { t } = useDesktopI18n() + const labelKey = `settings.fields.${schemaKey}.label` + const translatedLabel = t(labelKey) + + const label = + translatedLabel === labelKey + ? (FIELD_LABELS[schemaKey] ?? prettyName(schemaKey.split('.').pop() ?? schemaKey)) + : translatedLabel + const normalize = (v: string) => v.toLowerCase().replace(/[^a-z0-9]+/g, '') const rawDescription = (FIELD_DESCRIPTIONS[schemaKey] ?? schema.description ?? '').trim() const normalizedDesc = normalize(rawDescription) @@ -54,7 +63,9 @@ function ConfigField({ if (schema.type === 'boolean') { return row(
- {value ? 'On' : 'Off'} + + {value ? t('settings.config.on') : t('settings.config.off')} +
) @@ -77,8 +88,8 @@ function ConfigField({ {option ? (optionLabels?.[option] ?? prettyName(option)) : schemaKey === 'display.personality' - ? 'None' - : '(none)'} + ? t('settings.config.none') + : t('settings.config.noneParens')} ))} @@ -98,7 +109,7 @@ function ConfigField({ onChange(n) } }} - placeholder="Not set" + placeholder={t('settings.config.notSet')} type="number" value={value === undefined || value === null ? '' : String(value)} /> @@ -117,7 +128,7 @@ function ConfigField({ .filter(Boolean) ) } - placeholder="comma-separated values" + placeholder={t('settings.config.commaSeparated')} value={Array.isArray(value) ? value.join(', ') : String(value ?? '')} /> ) @@ -134,7 +145,7 @@ function ConfigField({ /* keep last valid */ } }} - placeholder="Not set" + placeholder={t('settings.config.notSet')} spellCheck={false} value={JSON.stringify(value, null, 2)} />, @@ -149,14 +160,14 @@ function ConfigField({