diff --git a/apps/web/components/onboarding/step-agents.tsx b/apps/web/components/onboarding/step-agents.tsx index 7f6f596d56..5d1151b7cc 100644 --- a/apps/web/components/onboarding/step-agents.tsx +++ b/apps/web/components/onboarding/step-agents.tsx @@ -17,6 +17,7 @@ import { AgentLogo } from "@/components/agent-logo"; import { ProfileFormFields, type ProfileFormData } from "@/components/settings/profile-form-fields"; import type { AvailableAgent, ToolStatus } from "@/lib/types/http"; import { copyToClipboard } from "@/lib/utils/copy-to-clipboard"; +import { Trans, useTranslation } from "react-i18next"; export type AgentSetting = { profileId: string; @@ -49,12 +50,13 @@ function CopyButton({ text }: { text: string }) { } function StatusPill({ status, error }: { status: string; error?: string }) { + const { t } = useTranslation(); switch (status) { case "auth_required": { const pill = ( - No auth + {t("common:noAuth")} ); if (!error) return pill; @@ -67,13 +69,15 @@ function StatusPill({ status, error }: { status: string; error?: string }) { } case "not_installed": return ( - Not installed + + {t("common:notInstalled")} + ); case "failed": { const pill = ( - Error + {t("common:error")} ); if (!error) return pill; @@ -88,16 +92,20 @@ function StatusPill({ status, error }: { status: string; error?: string }) { return ( - Probing + {t("common:probing")} ); case "not_configured": - return Pending; + return ( + + {t("common:pending")} + + ); default: return ( - Installed + {t("common:installed")} ); } @@ -172,10 +180,13 @@ function NotInstalledItems({ tools: ToolStatus[]; showLabel: boolean; }) { + const { t } = useTranslation(); if (agents.length === 0 && tools.length === 0) return null; return ( <> - {showLabel &&

Not yet installed

} + {showLabel && ( +

{t("common:notYetInstalled")}

+ )} {agents.map((agent) => (
) => void; }) { + const { t } = useTranslation(); const [openAgent, setOpenAgent] = useState(null); if (loading) { return (
- Discovering agents... + {t("common:discoveringAgents")}
); } @@ -281,13 +293,12 @@ export function StepAgents({ showLabel={installedAgents.length > 0} />
+

{t("common:onboardingAgentsHelp")}

- Expand an agent to configure its model and permissions. Changes are saved when you proceed. - You can also add custom TUI agents later in Settings > Agents. -

-

- Careful: The default Agent Profiles run - with Auto Approve enabled (YOLO mode). + + Careful: The default Agent Profiles + run with Auto Approve enabled (YOLO mode). +

); diff --git a/apps/web/components/release-notes/release-notes-button.tsx b/apps/web/components/release-notes/release-notes-button.tsx index 5c5d03f619..7eea92869e 100644 --- a/apps/web/components/release-notes/release-notes-button.tsx +++ b/apps/web/components/release-notes/release-notes-button.tsx @@ -4,6 +4,7 @@ import type { ComponentProps } from "react"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { IconSparkles } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; type ReleaseNotesButtonProps = { hasUnseen: boolean; @@ -12,6 +13,7 @@ type ReleaseNotesButtonProps = { }; export function ReleaseNotesButton({ hasUnseen, onClick, size = "icon" }: ReleaseNotesButtonProps) { + const { t } = useTranslation(); return ( @@ -20,10 +22,10 @@ export function ReleaseNotesButton({ hasUnseen, onClick, size = "icon" }: Releas {hasUnseen && ( )} - What's New + {t("common:whatSNew")} - What's New + {t("common:whatSNew")} ); } diff --git a/apps/web/components/release-notes/release-notes-dialog.tsx b/apps/web/components/release-notes/release-notes-dialog.tsx index 82b61580a1..b670b427be 100644 --- a/apps/web/components/release-notes/release-notes-dialog.tsx +++ b/apps/web/components/release-notes/release-notes-dialog.tsx @@ -15,6 +15,8 @@ import { IconExternalLink } from "@tabler/icons-react"; import { remarkPlugins, markdownComponents } from "@/components/shared/markdown-components"; import { getReleaseUrl } from "@/lib/release-notes"; import type { ChangelogEntry } from "@/lib/changelog"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; type ReleaseNotesDialogProps = { open: boolean; @@ -23,11 +25,13 @@ type ReleaseNotesDialogProps = { latestVersion: string; }; -function buildDescription(entries: ChangelogEntry[]): string { +function buildDescription(t: TFunction, entries: ChangelogEntry[]): string { if (entries.length === 1) { - return entries[0].date ? `Released on ${entries[0].date}` : "Latest release notes"; + return entries[0].date + ? t("common:releasedOn", { date: entries[0].date }) + : t("common:latestReleaseNotes"); } - return `${entries.length} new releases`; + return t("common:newReleases", { count: entries.length }); } export function ReleaseNotesDialog({ @@ -36,6 +40,7 @@ export function ReleaseNotesDialog({ entries, latestVersion, }: ReleaseNotesDialogProps) { + const { t } = useTranslation(); const releaseUrl = getReleaseUrl(latestVersion); return ( @@ -43,10 +48,10 @@ export function ReleaseNotesDialog({ - What's New + {t("common:whatSNew")} v{latestVersion} - {buildDescription(entries)} + {buildDescription(t, entries)}
@@ -77,7 +82,7 @@ export function ReleaseNotesDialog({ rel="noopener noreferrer" className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer" > - View full release on GitHub + {t("common:viewFullReleaseOnGithub")}
diff --git a/apps/web/components/search/panel-search-bar.tsx b/apps/web/components/search/panel-search-bar.tsx index a05623d56c..0233a15b1c 100644 --- a/apps/web/components/search/panel-search-bar.tsx +++ b/apps/web/components/search/panel-search-bar.tsx @@ -13,6 +13,7 @@ import { IconLoader2, } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; export type MatchInfo = { current: number; @@ -89,6 +90,7 @@ function ActionButtons({ onPrev: () => void; onClose: () => void; }) { + const { t } = useTranslation(); return ( <> {detailExpanded && (
@@ -125,6 +128,7 @@ function StepMessages({ step }: { step: PrepareStepInfo }) {
 }
 
 function StepRow({ step }: { step: PrepareStepInfo }) {
+  const { t } = useTranslation();
   const inlineCommand = step.command && isInlineCommand(step.command) ? step.command : undefined;
   const blockCommand = step.command && !isInlineCommand(step.command) ? step.command : undefined;
   const hasExpandable = Boolean(step.output) || Boolean(blockCommand);
@@ -144,7 +148,7 @@ function StepRow({ step }: { step: PrepareStepInfo }) {
         
- {step.name || "Preparing..."} + {step.name || t("task:preparingEllipsis")} {inlineCommand && ( {inlineCommand} @@ -279,20 +283,24 @@ function HeaderIcon({ status }: { status: EffectiveStatus }) { return ; } -function getHeaderLabel(status: EffectiveStatus, prepareState: SessionPrepareState): string { - if (status === "preparing") return "Preparing environment..."; - if (status === "failed") return "Environment setup failed"; +function getHeaderLabel( + t: TFunction, + status: EffectiveStatus, + prepareState: SessionPrepareState, +): string { + if (status === "preparing") return t("task:preparingEnvironmentEllipsis"); + if (status === "failed") return t("task:environmentSetupFailed"); if (status === "completed_with_error") { - return "Environment setup finished with errors"; + return t("task:environmentSetupFinishedWithErrors"); } if (status === "completed_with_warnings") { const warningSteps = prepareState.steps.filter((s) => s.warning); if (warningSteps.length === 1 && isFallbackNoticeStep(warningSteps[0])) { - return "Environment prepared on a fresh sandbox"; + return t("task:environmentPreparedOnFreshSandbox"); } - return "Environment prepared with warnings"; + return t("task:environmentPreparedWithWarnings"); } - return "Environment prepared"; + return t("task:environmentPrepared"); } function InfoLine({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { @@ -347,7 +355,7 @@ type ScriptMetadata = { // Convert a setup-script `script_execution` message into the same step shape // the existing prepare panel renders, so it slots in alongside Validate/Sync/ // Create-worktree without a parallel render path. -function setupScriptMessageToStep(message: Message): PrepareStepInfo { +function setupScriptMessageToStep(t: TFunction, message: Message): PrepareStepInfo { const meta = (message.metadata ?? {}) as ScriptMetadata; const command = meta.command ?? ""; const status = scriptStatusToStepStatus(meta.status, meta.exit_code); @@ -355,11 +363,11 @@ function setupScriptMessageToStep(message: Message): PrepareStepInfo { status === "failed" ? meta.error || (meta.exit_code !== undefined && meta.exit_code !== 0 - ? `Script exited with code ${meta.exit_code}` - : "Script failed") + ? t("task:scriptExitedWithCode", { code: meta.exit_code }) + : t("task:scriptFailed")) : undefined; return { - name: "Run repository setup script", + name: t("task:runRepositorySetupScript"), command, status, output: message.content || undefined, @@ -370,9 +378,10 @@ function setupScriptMessageToStep(message: Message): PrepareStepInfo { } function useSetupScriptSteps(sessionId: string): PrepareStepInfo[] { + const { t } = useTranslation(); const messages = useAppStore((state) => state.messages.bySession[sessionId]); if (!messages || messages.length === 0) return []; - return messages.filter(isSetupScriptMessage).map(setupScriptMessageToStep); + return messages.filter(isSetupScriptMessage).map((m) => setupScriptMessageToStep(t, m)); } function SessionInfo({ sessionId }: { sessionId: string }) { @@ -389,24 +398,31 @@ function SessionInfo({ sessionId }: { sessionId: string }) {
{isWorktree && worktree_path && ( }> - Isolated worktree at {worktree_path} + + Isolated worktree at {worktree_path} + )} {!isWorktree && worktree_path && ( }> - Working in {worktree_path} + + Working in {worktree_path} + )} {worktree_branch && ( }> {base_branch ? ( - <> + Branch {worktree_branch}, based on {base_branch} - + ) : ( - <> + On branch {worktree_branch} - + )} )} @@ -461,6 +477,7 @@ function hasPrepareDetails( } export function PrepareProgress({ sessionId }: PrepareProgressProps) { + const { t } = useTranslation(); const { status, prepareState } = usePrepareStatus(sessionId); const session = useAppStore((state) => state.taskSessions.items[sessionId]); const setupScriptSteps = useSetupScriptSteps(sessionId); @@ -501,7 +518,7 @@ export function PrepareProgress({ sessionId }: PrepareProgressProps) { // belong after Create-worktree finishes. Appending keeps the existing prepare // step order intact. const visibleSteps = [...prepareState.steps.filter(isVisibleStep), ...setupScriptSteps]; - const headerLabel = getHeaderLabel(status, prepareState); + const headerLabel = getHeaderLabel(t, status, prepareState); const isErrorStatus = status === "failed" || status === "completed_with_error"; const headerClass = cn("text-xs", isErrorStatus ? "text-destructive" : "text-muted-foreground"); const hasFailureDetails = isErrorStatus && Boolean(prepareState.errorMessage); @@ -532,14 +549,16 @@ export function PrepareProgress({ sessionId }: PrepareProgressProps) { )}
diff --git a/apps/web/components/settings/agent-card.tsx b/apps/web/components/settings/agent-card.tsx index 1441bceb01..37e1e8f1c5 100644 --- a/apps/web/components/settings/agent-card.tsx +++ b/apps/web/components/settings/agent-card.tsx @@ -5,12 +5,14 @@ import { IconRobot, IconChevronRight } from "@tabler/icons-react"; import { Card, CardContent } from "@kandev/ui/card"; import { Badge } from "@kandev/ui/badge"; import type { AgentProfile } from "@/lib/settings/types"; +import { useTranslation } from "react-i18next"; type AgentCardProps = { agent: AgentProfile; }; export function AgentCard({ agent }: AgentCardProps) { + const { t } = useTranslation(); const agentLabel = agent.agentDisplayName; return ( @@ -30,13 +32,13 @@ export function AgentCard({ agent }: AgentCardProps) { {agent.autoApprove && ( - Auto-approve + {t("settings:autoApprove")} )}
-

Model: {agent.model}

-

Temperature: {agent.temperature}

+

{t("settings:modelWithValue", { model: agent.model })}

+

{t("settings:temperatureWithValue", { temperature: agent.temperature })}

diff --git a/apps/web/components/shared/mermaid-block.tsx b/apps/web/components/shared/mermaid-block.tsx index c32d262133..0bcff24302 100644 --- a/apps/web/components/shared/mermaid-block.tsx +++ b/apps/web/components/shared/mermaid-block.tsx @@ -12,6 +12,7 @@ import { import { useMermaidScale, useMermaidViewportWidth } from "./mermaid-block-hooks"; import { useToast } from "@/components/toast-provider"; import { showMermaidErrorToast } from "./mermaid-error-toast"; +import { useTranslation } from "react-i18next"; type MermaidAPI = typeof import("mermaid").default; @@ -119,6 +120,7 @@ function useMermaidRender( } export function MermaidBlock({ code, taskId }: MermaidBlockProps) { + const { t } = useTranslation(); const containerRef = useRef(null); const svgSizeRef = useRef<{ w: number; h: number } | null>(null); const [svgSize, setSvgSize] = useState<{ w: number; h: number } | null>(null); @@ -164,7 +166,7 @@ export function MermaidBlock({ code, taskId }: MermaidBlockProps) { if (error !== null && svg === null) { return (
-

Failed to render diagram

+

{t("common:failedToRenderDiagram")}

{code}
); @@ -229,6 +231,7 @@ function MermaidToolbar({ onReset, onToggleCode, }: MermaidToolbarProps) { + const { t } = useTranslation(); return (
{!showCode && onZoomOut && onReset && onZoomIn && scale != null && ( @@ -261,7 +264,7 @@ function MermaidToolbar({ type="button" onClick={onToggleCode} className="p-0.5 text-muted-foreground hover:text-foreground cursor-pointer" - title={showCode ? "Show diagram" : "Show code"} + title={showCode ? t("common:showDiagram") : t("common:showCode")} > diff --git a/apps/web/components/system-health/health-indicator.tsx b/apps/web/components/system-health/health-indicator.tsx index b2ab5e926b..004fd6faca 100644 --- a/apps/web/components/system-health/health-indicator.tsx +++ b/apps/web/components/system-health/health-indicator.tsx @@ -12,6 +12,7 @@ import { DialogDescription, } from "@kandev/ui/dialog"; import { IconAlertTriangle, IconExternalLink } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; import { useAppStore } from "@/components/state-provider"; import type { HealthIssue } from "@/lib/types/health"; @@ -26,6 +27,7 @@ export function HealthIndicatorButton({ onClick, size = "icon", }: HealthIndicatorButtonProps) { + const { t } = useTranslation(); if (!hasIssues) return null; return ( @@ -34,10 +36,10 @@ export function HealthIndicatorButton({ - Setup Issues + {t("system:setupIssues")} ); } @@ -49,6 +51,7 @@ type HealthIssuesDialogProps = { }; export function HealthIssuesDialog({ open, onOpenChange, issues }: HealthIssuesDialogProps) { + const { t } = useTranslation(); const router = useRouter(); const workspaceId = useAppStore((state) => state.workspaces.activeId); @@ -60,12 +63,10 @@ export function HealthIssuesDialog({ open, onOpenChange, issues }: HealthIssuesD - Setup Issues + {t("system:setupIssues")} - {issues.length === 1 - ? "1 issue needs your attention" - : `${issues.length} issues need your attention`} + {t("system:issuesNeedAttention", { count: issues.length })}
diff --git a/apps/web/components/system-metrics/status-surface-metrics.tsx b/apps/web/components/system-metrics/status-surface-metrics.tsx index bad6f6d983..2d000422b8 100644 --- a/apps/web/components/system-metrics/status-surface-metrics.tsx +++ b/apps/web/components/system-metrics/status-surface-metrics.tsx @@ -11,6 +11,8 @@ import { } from "@tabler/icons-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { formatDistanceToNow } from "date-fns"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import { useAppStore } from "@/components/state-provider"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useSystemMetricsSubscription } from "@/hooks/use-system-metrics-subscription"; @@ -27,6 +29,7 @@ export function StatusSurfaceMetrics({ density, drawerOpen, }: StatusSurfaceMetricsProps) { + const { t } = useTranslation(); // Wire/storage name stays stable for existing user settings and API payloads. const enabled = useAppStore((state) => state.userSettings.systemMetricsDisplay.showInTopbar); const simplified = useAppStore((state) => state.userSettings.systemMetricsDisplay.simplified); @@ -44,10 +47,10 @@ export function StatusSurfaceMetrics({

- System metrics + {t("system:systemMetrics")}

{!host ? ( @@ -66,7 +69,7 @@ export function StatusSurfaceMetrics({
{!host ? ( @@ -84,6 +87,7 @@ export function StatusSurfaceMetrics({ } function EmptyMetrics({ drawer = false }: { drawer?: boolean }) { + const { t } = useTranslation(); return (
- Metrics unavailable + {t("system:metricsUnavailable")}
); } @@ -154,19 +158,23 @@ function SourceBadge({ updatedAt?: string; showLabel: boolean; }) { + const { t } = useTranslation(); return ( - + - {showLabel ? Host : null} + {showLabel ? {t("system:host")} : null}
-
Host
+
{t("system:host")}
{source.label}
-
{lastUpdatedText(updatedAt)}
+
{lastUpdatedText(t, updatedAt)}
@@ -212,16 +220,14 @@ function MetricValue({ updatedAt?: string; simplified: boolean; }) { - const help = - metric.id === "io_load" - ? "Average number of tasks running or waiting for CPU during the last minute. Compare this value with the host's CPU core count." - : null; + const { t } = useTranslation(); + const help = metric.id === "io_load" ? t("system:ioLoadHelp") : null; return ( {metricIcon(metric.id)} {!simplified ? : null} @@ -232,28 +238,32 @@ function MetricValue({
-
{metricLabel(metric.id)}
+
{metricLabel(t, metric.id)}
{help ?
{help}
: null} -
Host: {source.label}
+
+ {t("system:hostWithLabel", { label: source.label })} +
{formatMetric(metric)}
{metric.error ? (
{metric.error}
) : null} -
{lastUpdatedText(updatedAt)}
+
{lastUpdatedText(t, updatedAt)}
); } -function metricLabel(id: string) { +// The map keys are wire metric ids and the `?? id` fallback returns an +// untranslated id on purpose — both are protocol. Only the values are copy. +function metricLabel(t: TFunction, id: string) { return ( { - cpu_percent: "CPU", - memory_percent: "Memory", - disk_percent: "Disk", - cpu_temp: "CPU temperature", - io_load: "System load (1 min)", + cpu_percent: t("system:metricCpuPercent"), + memory_percent: t("system:metricMemoryPercent"), + disk_percent: t("system:metricDiskPercent"), + cpu_temp: t("system:metricCpuTemp"), + io_load: t("system:metricIoLoad"), }[id] ?? id ); } @@ -299,9 +309,11 @@ function metricColor(metric: SystemMetricSample) { return "text-current"; } -function lastUpdatedText(updatedAt?: string) { - if (!updatedAt) return "Last update unknown"; +function lastUpdatedText(t: TFunction, updatedAt?: string) { + if (!updatedAt) return t("system:lastUpdateUnknown"); const date = new Date(updatedAt); - if (Number.isNaN(date.getTime())) return "Last update unknown"; - return `Updated ${formatDistanceToNow(date, { addSuffix: true })}`; + if (Number.isNaN(date.getTime())) return t("system:lastUpdateUnknown"); + return t("system:updatedRelative", { + relative: formatDistanceToNow(date, { addSuffix: true }), + }); } diff --git a/apps/web/components/ui/data-table-pagination.tsx b/apps/web/components/ui/data-table-pagination.tsx index 3fea0729cd..98997e9879 100644 --- a/apps/web/components/ui/data-table-pagination.tsx +++ b/apps/web/components/ui/data-table-pagination.tsx @@ -15,6 +15,7 @@ import { IconChevronsLeft, IconChevronsRight, } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; interface DataTablePaginationProps { table: Table; @@ -71,6 +72,7 @@ function PageButtons({ } export function DataTablePagination({ table }: DataTablePaginationProps) { + const { t } = useTranslation(); const currentPage = table.getState().pagination.pageIndex + 1; const pageCount = table.getPageCount(); const pages = getPageNumbers(currentPage, pageCount); @@ -79,17 +81,16 @@ export function DataTablePagination({ table }: DataTablePaginationProps
- {table.getFilteredRowModel().rows.length > 0 && ( - <> - Showing {pagination.pageIndex * pagination.pageSize + 1} to{" "} - {Math.min((pagination.pageIndex + 1) * pagination.pageSize, table.getRowCount())} of{" "} - {table.getRowCount()} results - - )} + {table.getFilteredRowModel().rows.length > 0 && + t("common:showingRange", { + from: pagination.pageIndex * pagination.pageSize + 1, + to: Math.min((pagination.pageIndex + 1) * pagination.pageSize, table.getRowCount()), + count: table.getRowCount(), + })}
-

Rows per page

+

{t("common:rowsPerPage")}