diff --git a/dashboard/src/v2/DashboardV2.tsx b/dashboard/src/v2/DashboardV2.tsx index 15946ee7eb..369dc68a00 100644 --- a/dashboard/src/v2/DashboardV2.tsx +++ b/dashboard/src/v2/DashboardV2.tsx @@ -13,6 +13,8 @@ import { PageHeader } from "./components/layout/PageHeader.js"; import { Hexagon } from "lucide-preact"; import { SectionDivider } from "./components/ui/SectionDivider.js"; +import { useDashboardI18n } from "./i18n/index.js"; +import { overviewMessages } from "./i18n/messages/overview.js"; const OverviewTelemetry = lazy(() => import("./components/OverviewTelemetry.js").then(m => ({ default: m.OverviewTelemetry }))); @@ -20,6 +22,7 @@ export const DashboardV2: FunctionComponent = () => { const mainContentRef = useRef(null); const pageData = useOverviewPageData(); const prefersReducedMotion = useReducedMotion(); + const { translate } = useDashboardI18n(); useLayoutEffect(() => { const ctx = gsap.context(() => { @@ -39,48 +42,48 @@ export const DashboardV2: FunctionComponent = () => { }, [prefersReducedMotion]); return ( - -
Overview route loaded
+ +
{translate(overviewMessages, "routeLoaded")}
{/* Page Header */} +
} /> {/* Metrics Section */} -
+
{/* Section Divider */} - + {/* Main Grid */}
{/* Sources and Tasks */}
-
+
-
+
{/* Live Telemetry */} -
}> diff --git a/dashboard/src/v2/TasksPage.tsx b/dashboard/src/v2/TasksPage.tsx index 35f17a001f..3cef78d1ab 100644 --- a/dashboard/src/v2/TasksPage.tsx +++ b/dashboard/src/v2/TasksPage.tsx @@ -24,10 +24,6 @@ import { TaskBoardFilters } from "./components/tasks/TaskBoardFilters.js"; import { TaskBoardColumns } from "./components/tasks/TaskBoardColumns.js"; import { useInteractionTokens } from "./lib/motion/tokens.js"; import { useTaskBoardController } from "./hooks/use-task-board-controller.js"; -import { useOptionalDashboardI18n } from "./i18n/context.js"; -import { taskMessages } from "./i18n/messages/tasks.js"; -import { getTaskPriorityLabel, getTaskStatusLabel } from "./lib/tasks-constants.js"; -import { formatTaskSprintDateRange } from "./lib/tasks/task-presentation.js"; type TaskScopePlaceholderMode = "project" | "sprint"; @@ -36,13 +32,12 @@ const TaskScopePlaceholder: FunctionComponent<{ hasProjects: boolean; onAddProject: () => void; }> = ({ mode, hasProjects, onAddProject }) => { - const { translate } = useOptionalDashboardI18n(); const isProjectMode = mode === "project"; - const title = translate(taskMessages, isProjectMode ? "taskNeedsProject" : "taskNeedsSprint"); - const eyebrow = translate(taskMessages, isProjectMode ? "taskProjectStandby" : "taskSprintRequired"); + const title = isProjectMode ? "Task work starts with a project." : "Create a sprint to unlock tasks."; + const eyebrow = isProjectMode ? "Task Board Standby" : "Sprint Scope Required"; const body = isProjectMode - ? translate(taskMessages, "taskNeedsProjectBody") - : translate(taskMessages, "taskNeedsSprintBody"); + ? "Connect a project before the task board starts tracking queued work, active implementation, QA review, and completed delivery." + : "Tasks are organized inside sprint scope. Create or select a sprint before adding implementation work to the board."; return (
@@ -77,7 +72,7 @@ const TaskScopePlaceholder: FunctionComponent<{ icon={Plus} className="!inline-flex !min-h-[44px] !items-center !gap-2.5 !rounded-full !px-5 !py-2.5 !text-[10px] !font-bold !uppercase !tracking-[0.14em] !shadow-[0_10px_30px_rgba(0,224,160,0.22)] hover:!-translate-y-px focus-visible:!ring-2 focus-visible:!ring-signal-500/40" > - {translate(taskMessages, hasProjects ? "addProject" : "addFirstProject")} + {hasProjects ? "Add Project" : "Add First Project"} ) : ( - {translate(taskMessages, "planSprint")} + Plan Sprint )} - {translate(taskMessages, isProjectMode ? "manageProjects" : "openSprints")} + {isProjectMode ? "Manage Projects" : "Open Sprints"} @@ -103,9 +98,9 @@ const TaskScopePlaceholder: FunctionComponent<{
{[ - { label: translate(taskMessages, "project"), value: translate(taskMessages, isProjectMode ? "required" : "ready"), tone: isProjectMode ? "text-ember-500" : "text-status-green" }, - { label: translate(taskMessages, "sprint"), value: translate(taskMessages, isProjectMode ? "waiting" : "required"), tone: isProjectMode ? "text-signal-500" : "text-ember-500" }, - { label: translate(taskMessages, "tasks"), value: translate(taskMessages, "locked"), tone: "text-slate-500 dark:text-slate-400" }, + { label: "Project", value: isProjectMode ? "required" : "ready", tone: isProjectMode ? "text-ember-500" : "text-status-green" }, + { label: "Sprint", value: isProjectMode ? "waiting" : "required", tone: isProjectMode ? "text-signal-500" : "text-ember-500" }, + { label: "Tasks", value: "locked", tone: "text-slate-500 dark:text-slate-400" }, ].map((item, index) => (
= memo(({ sprint, tasks }) => { - const { locale, translate, formatNumber } = useOptionalDashboardI18n(); const completed = tasks.filter((task) => task.status === "completed").length; const inProgress = tasks.filter((task) => task.status === "in_progress").length; const pending = tasks.filter((task) => task.status === "pending").length; @@ -143,7 +137,7 @@ const SprintProgressCard: FunctionComponent<{ return (
- {formatNumber(pct)}% + {pct}%
@@ -152,7 +146,7 @@ const SprintProgressCard: FunctionComponent<{

{sprint.name}

-

{formatTaskSprintDateRange(sprint.startDate, sprint.endDate, locale)}

+

{sprint.date}

@@ -162,7 +156,7 @@ const SprintProgressCard: FunctionComponent<{ aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} - aria-label={translate(taskMessages, "sprintProgress", { percent: formatNumber(pct) })} + aria-label={`Sprint progress: ${pct}%`} > {completed > 0 &&
} {inProgress > 0 &&
} @@ -171,12 +165,12 @@ const SprintProgressCard: FunctionComponent<{
{[ - { label: translate(taskMessages, "completed"), value: completed, color: "text-status-green" }, - { label: translate(taskMessages, "running"), value: inProgress, color: "text-signal-500" }, - { label: translate(taskMessages, "queued"), value: pending, color: "text-slate-400" }, + { label: "Completed", value: completed, color: "text-status-green" }, + { label: "Running", value: inProgress, color: "text-signal-500" }, + { label: "Queued", value: pending, color: "text-slate-400" }, ].map(({ label, value, color }) => (
- {formatNumber(value)} + {value} {label}
))} @@ -187,7 +181,7 @@ const SprintProgressCard: FunctionComponent<{ className="flex items-center gap-1.5 mt-5 pt-4 border-t border-black/[0.05] dark:border-white/[0.04] text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 hover:text-ember-500 transition-colors duration-200 group/link" > - {translate(taskMessages, "viewSprint")} + View Sprint
); @@ -197,7 +191,6 @@ export const TasksPage: FunctionComponent = () => { const headerRef = useRef(null); const boardRef = useRef(null); const interactionTokens = useInteractionTokens(); - const { locale, translate, formatNumber } = useOptionalDashboardI18n(); const controller = useTaskBoardController(); const { projects, @@ -330,32 +323,27 @@ export const TasksPage: FunctionComponent = () => { return () => ctx.revert(); }, [clearResolvedTaskId, resolvedTaskId, tasks]); - const selectedSprintNumber = selectedSprintModel?.number; - return ( {selectedProject ? taskScopeSprintId - ? translate(taskMessages, "selectedProjectSprintSubtitle", { project: selectedProject.name, sprint: selectedSprintNumber == null ? "..." : formatNumber(selectedSprintNumber) }) - : translate(taskMessages, "selectedProjectSubtitle", { project: selectedProject.name }) - : translate(taskMessages, "noProjectSubtitle")} + ? `Task execution for ${selectedProject.name}, scoped to Sprint ${sprints.find((sprint) => sprint.id === taskScopeSprintId)?.number ?? "..."}.` + : `Task execution for ${selectedProject.name}. Showing all tasks across the project.` + : "Select a project to manage sprint tasks."} {selectedProject && (statusFilter !== "all" || priorityFilter !== "all") && ( - {translate(taskMessages, "filteredSubtitle", { - status: statusFilter !== "all" ? getTaskStatusLabel(statusFilter, locale) : translate(taskMessages, "all"), - priority: priorityFilter !== "all" ? getTaskPriorityLabel(priorityFilter, locale) : translate(taskMessages, "anyPriority"), - })} + Filtered to show {statusFilter !== "all" ? statusFilter.replace("_", " ") : "all"} status and {priorityFilter !== "all" ? priorityFilter : "any"} priority. )} @@ -368,18 +356,18 @@ export const TasksPage: FunctionComponent = () => { - {formatNumber(stats.inProgress)} {translate(taskMessages, "running")} + {stats.inProgress} Running
)} {stats.critical > 0 && (
- {formatNumber(stats.critical)} {translate(taskMessages, "critical")} + {stats.critical} Critical
)}
- {formatNumber(stats.total)} {translate(taskMessages, "total")} + {stats.total} Total
@@ -390,7 +378,7 @@ export const TasksPage: FunctionComponent = () => { disabled={!selectedProject || sprints.length === 0} className="!flex !items-center !justify-center !w-full lg:!w-auto !gap-2.5 !px-6 !py-3.5 !font-bold !text-sm !rounded-2xl !transition-all !duration-300 !shadow-[0_4px_20px_rgba(0,224,160,0.25)] hover:!shadow-[0_8px_32px_rgba(0,224,160,0.45)] hover:!-translate-y-px !shrink-0" > - {translate(taskMessages, (showComposer || editingTask) ? "closeComposer" : "newTask")} + {(showComposer || editingTask) ? "Close Composer" : "New Task"}
} @@ -417,13 +405,13 @@ export const TasksPage: FunctionComponent = () => { aria-labelledby="task-workspace-heading" className={`grid items-start gap-6 ${showComposer || editingTask ? "xl:grid-cols-[minmax(0,1fr)_minmax(28rem,42vw)]" : "grid-cols-1"}`} > -

{translate(taskMessages, "taskWorkspace")}

+

Task workspace

-

{translate(taskMessages, "taskBoardRegion")}

+

Task board

{
{filterTransitionPending && (
- {translate(taskMessages, "updatingFilters")} + Updating task board filters. Current cards remain visible until results settle.
)} { className="order-1 min-w-0 scroll-mt-8 xl:sticky xl:top-6 xl:order-2" >

- {translate(taskMessages, editingTask ? "editTaskEditor" : "newTaskEditor")} + {editingTask ? "Edit task editor" : "New task editor"}

= ({ points, color }) => (
}> = ({ pageData }) => { const { projects, selectedProject, sprints, tasks, stats: statsSnapshot, isLoading } = pageData; + const { formatNumber, translate, translatePlural } = useDashboardI18n(); const stats = useMemo(() => computeOverviewStats(projects, sprints, tasks, statsSnapshot), [projects, sprints, tasks, statsSnapshot]); const usage = statsSnapshot?.usage; const totalTasks = stats.completedTasks + stats.openTasks; const completionRate = totalTasks > 0 ? Math.round((stats.completedTasks / totalTasks) * 100) : 0; const completedSprints = Math.max(stats.totalSprints - stats.activeSprints, 0); - const activeTime = formatStatsDuration(usage?.activeTimeMs ?? 0); + const activeTime = formatOverviewDuration(usage?.activeTimeMs ?? 0, formatNumber); const invocationCount = usage?.invocationCount ?? 0; const activeSprintLabel = statsSnapshot?.activeSprint - ? `#${statsSnapshot.activeSprint.sprintNumber ?? "-"}` - : "None"; + ? `#${statsSnapshot.activeSprint.sprintNumber == null ? "-" : formatNumber(statsSnapshot.activeSprint.sprintNumber)}` + : translate(overviewMessages, "none"); if (isLoading) { return ( -
- Loading overview stats. +
+ {translate(overviewMessages, "loadingOverviewStatsAnnouncement")} @@ -42,110 +44,110 @@ export const HeaderStats: FunctionComponent<{ pageData: ReturnType +
Token telemetry active
} + trend={
{translate(overviewMessages, "tokenTelemetryActive")}
} >
- PROJECT - {selectedProject?.name || "None"} + {translate(overviewMessages, "project")} + {selectedProject?.name || translate(overviewMessages, "none")}
- COST - {formatCost(usage?.totalCostUsd ?? 0)} + {translate(overviewMessages, "cost")} + {formatOverviewCost(usage?.totalCostUsd ?? 0, formatNumber)}
- RUNS - {invocationCount.toLocaleString()} + {translate(overviewMessages, "runs")} + {formatNumber(invocationCount)}
Sprint telemetry available
} + trend={
{translate(overviewMessages, "sprintTelemetryAvailable")}
} >
- ACTIVE - {stats.activeSprints} + {translate(overviewMessages, "active")} + {formatNumber(stats.activeSprints)}
- COMPLETE - {completedSprints} + {translate(overviewMessages, "complete")} + {formatNumber(completedSprints)}
- CURRENT + {translate(overviewMessages, "current")} {activeSprintLabel}
{stats.runningTasks} live tasks
} - description={{stats.runningTasks} live} + trend={
{translatePlural(overviewMessages, "liveTaskCount", stats.runningTasks, { formattedCount: formatNumber(stats.runningTasks) })}
} + description={{translate(overviewMessages, "liveCount", { count: formatNumber(stats.runningTasks) })}} >
- RUNNING - {stats.runningTasks} + {translate(overviewMessages, "running")} + {formatNumber(stats.runningTasks)}
- CRITICAL - 0 ? "text-status-red" : "text-slate-700 dark:text-slate-300"}>{stats.criticalTasks} + {translate(overviewMessages, "critical")} + 0 ? "text-status-red" : "text-slate-700 dark:text-slate-300"}>{formatNumber(stats.criticalTasks)}
- HEALTH - 0 ? "text-status-red" : "text-signal-700 dark:text-signal-400"}>{stats.criticalTasks > 0 ? "Review" : "Clear"} + {translate(overviewMessages, "health")} + 0 ? "text-status-red" : "text-signal-700 dark:text-signal-400"}>{translate(overviewMessages, stats.criticalTasks > 0 ? "review" : "clear")}
- Completion telemetry updated + {translate(overviewMessages, "completionTelemetryUpdated")}
} description={ - {completionRate}% + {formatNumber(completionRate / 100, { style: "percent", maximumFractionDigits: 0 })} } >
- OPEN - {stats.openTasks} + {translate(overviewMessages, "open")} + {formatNumber(stats.openTasks)}
- TOTAL - {totalTasks} + {translate(overviewMessages, "total")} + {formatNumber(totalTasks)}
- ACTIVE TIME + {translate(overviewMessages, "activeTime")} {activeTime}
diff --git a/dashboard/src/v2/components/OverviewTelemetry.tsx b/dashboard/src/v2/components/OverviewTelemetry.tsx index 843640fbf6..16bd66b5d6 100644 --- a/dashboard/src/v2/components/OverviewTelemetry.tsx +++ b/dashboard/src/v2/components/OverviewTelemetry.tsx @@ -5,10 +5,11 @@ import { SkeletonPanel } from "./layout/SkeletonLoader.js"; import { useDashboardRuntimeData } from "../../hooks/use-dashboard-runtime-data.js"; import { useOverviewTelemetry } from "../../hooks/use-overview-telemetry.js"; import { useSprints } from "../../hooks/useSprints.js"; -import { formatTime } from "../../lib/time.js"; import { buildProjectLookup, getEventStyle, getInterventionContent } from "../lib/overview-telemetry-view-models.js"; import { useProjectData } from "../context/project-data.js"; import { AttentionQueueItemsList } from "./AttentionLedger.js"; +import { useDashboardI18n } from "../i18n/index.js"; +import { overviewMessages } from "../i18n/messages/overview.js"; export const OverviewTelemetry: FunctionComponent = () => { @@ -21,6 +22,7 @@ export const OverviewTelemetry: FunctionComponent = () => { { selectedSprintId }, ); const isLoading = telemetryLoading || projectsLoading; + const { formatNumber, formatTime, translate } = useDashboardI18n(); const hasActiveProjects = telemetry?.activeProjects?.length > 0; const hasAttentionProjects = telemetry?.attentionProjects?.length > 0; @@ -35,6 +37,22 @@ export const OverviewTelemetry: FunctionComponent = () => { () => (telemetry?.activeProjects ?? []).reduce((sum, project) => sum + (project.runningDispatchCount ?? 0), 0), [telemetry], ); + const eventLabels = useMemo(() => ({ + taskState: (state: string) => translate(overviewMessages, "eventTaskState", { state }), + sprintState: (state: string) => translate(overviewMessages, "eventSprintState", { state }), + sprintPaused: translate(overviewMessages, "eventSprintPaused"), + states: { + failed: translate(overviewMessages, "eventStateFailed"), + completed: translate(overviewMessages, "eventStateCompleted"), + blocked: translate(overviewMessages, "eventStateBlocked"), + paused: translate(overviewMessages, "eventStatePaused"), + started: translate(overviewMessages, "eventStateStarted"), + running: translate(overviewMessages, "eventStateRunning"), + queued: translate(overviewMessages, "eventStateQueued"), + pending: translate(overviewMessages, "eventStatePending"), + in_progress: translate(overviewMessages, "taskStatusInProgress"), + }, + }), [translate]); if (error) { return ( @@ -42,7 +60,7 @@ export const OverviewTelemetry: FunctionComponent = () => {
@@ -62,17 +80,17 @@ export const OverviewTelemetry: FunctionComponent = () => { : "bg-slate-400 dark:bg-slate-500" }`} /> - {hasActiveProjects ? "Telemetry status: active projects running" : hasAnyAttentionSignal ? "Telemetry status: attention needed" : "Telemetry status: idle"} + {translate(overviewMessages, hasActiveProjects ? "telemetryStatusActive" : hasAnyAttentionSignal ? "telemetryStatusAttention" : "telemetryStatusIdle")}
- Telemetry. + {translate(overviewMessages, "telemetryTitle")}
{isLoading ? ( -
- Loading overview telemetry. +
+ {translate(overviewMessages, "loadingOverviewTelemetryAnnouncement")}
@@ -87,8 +105,8 @@ export const OverviewTelemetry: FunctionComponent = () => {
- Awaiting Runtime - No active project telemetry yet + {translate(overviewMessages, "awaitingRuntime")} + {translate(overviewMessages, "noActiveProjectTelemetry")}
) : ( @@ -96,20 +114,20 @@ export const OverviewTelemetry: FunctionComponent = () => { {/* Stat cards */}
-
Active
-
{telemetry?.activeProjects?.length ?? 0}
+
{translate(overviewMessages, "active")}
+
{formatNumber(telemetry?.activeProjects?.length ?? 0)}
-
-
{totalRunningDispatches}
+
+
{formatNumber(totalRunningDispatches)}
-
Attention
-
{telemetry?.attentionProjects?.length ?? 0}
+
{translate(overviewMessages, "attention")}
+
{formatNumber(telemetry?.attentionProjects?.length ?? 0)}
-
Events
-
{telemetry?.recentEvents?.length ?? 0}
+
{translate(overviewMessages, "events")}
+
{formatNumber(telemetry?.recentEvents?.length ?? 0)}
@@ -118,21 +136,21 @@ export const OverviewTelemetry: FunctionComponent = () => {
- {scopedAttentionItems.length} + {formatNumber(scopedAttentionItems.length)}

- {selectedProject?.name || "Selected project"} + {selectedProject?.name || translate(overviewMessages, "selectedProject")}

@@ -144,7 +162,7 @@ export const OverviewTelemetry: FunctionComponent = () => {
{telemetry.attentionProjects.map((project) => ( @@ -153,17 +171,17 @@ export const OverviewTelemetry: FunctionComponent = () => {
{project.projectName}
- {project.sprintName}{project.sprintNumber != null ? ` · Sprint ${project.sprintNumber}` : ""} + {project.sprintName}{project.sprintNumber != null ? ` · ${translate(overviewMessages, "sprintNumber", { number: formatNumber(project.sprintNumber) })}` : ""}
- {getInterventionContent(project) && ( + {getInterventionContent(project, translate(overviewMessages, "humanInterventionRequired")) && (
- {getInterventionContent(project)!.title} + {getInterventionContent(project, translate(overviewMessages, "humanInterventionRequired"))!.title}
)}
@@ -178,8 +196,8 @@ export const OverviewTelemetry: FunctionComponent = () => { {telemetry?.activeProjects?.length > 0 && (
- Active Sprints - {telemetry.activeProjects.length} + {translate(overviewMessages, "activeSprints")} + {formatNumber(telemetry.activeProjects.length)}
{telemetry.activeProjects.map((project) => { @@ -196,14 +214,14 @@ export const OverviewTelemetry: FunctionComponent = () => {
{project.projectName}
- {project.sprintName}{project.sprintNumber != null ? ` · Sprint ${project.sprintNumber}` : ""} + {project.sprintName}{project.sprintNumber != null ? ` · ${translate(overviewMessages, "sprintNumber", { number: formatNumber(project.sprintNumber) })}` : ""}
{active > 0 && ( @@ -219,22 +237,22 @@ export const OverviewTelemetry: FunctionComponent = () => { )}
-
Runtime Timeline
-
+
{translate(overviewMessages, "runtimeTimeline")}
+
{telemetry?.recentEvents?.map((event) => { - const style = getEventStyle(event); + const style = getEventStyle(event, eventLabels); return (
{style.label}
-
{formatTime(event.createdAt)}
+
{formatTime(new Date(event.createdAt), { hour: "2-digit", minute: "2-digit" })}
- {projectLookup.get(event.projectId) || "Project"} + {projectLookup.get(event.projectId) || translate(overviewMessages, "fallbackProject")}
- {event.sprintName}{event.sprintNumber != null ? ` · Sprint ${event.sprintNumber}` : ""} + {event.sprintName}{event.sprintNumber != null ? ` · ${translate(overviewMessages, "sprintNumber", { number: formatNumber(event.sprintNumber) })}` : ""}
); diff --git a/dashboard/src/v2/components/SourcesGrid.tsx b/dashboard/src/v2/components/SourcesGrid.tsx index 804f6716df..67344f7697 100644 --- a/dashboard/src/v2/components/SourcesGrid.tsx +++ b/dashboard/src/v2/components/SourcesGrid.tsx @@ -7,6 +7,9 @@ import { SourceCell } from "./ui/SourceCell.js"; import { SkeletonCard } from "./layout/SkeletonLoader.js"; import { useProjectData } from "../context/project-data.js"; import { useReducedMotion } from "../hooks/use-reduced-motion.js"; +import { EmptyState } from "./ui/EmptyState.js"; +import { useDashboardI18n } from "../i18n/index.js"; +import { overviewMessages } from "../i18n/messages/overview.js"; const DEFAULT_VISIBLE_SOURCE_CELLS = 5; const COMPACT_VISIBLE_SOURCE_CELLS = 4; @@ -54,6 +57,7 @@ export const SourcesGrid: FunctionComponent = () => { const [availableColumns, setAvailableColumns] = useState(DEFAULT_VISIBLE_SOURCE_CELLS); const { projects, loading: projectsLoading } = useProjectData(); const prefersReducedMotion = useReducedMotion(); + const { translate } = useDashboardI18n(); useLayoutEffect(() => { if (containerRef.current) { @@ -105,11 +109,11 @@ export const SourcesGrid: FunctionComponent = () => { }, [layoutPlan.visibleCount, projects]); return ( -
+
} - title="Projects & Sources" + title={translate(overviewMessages, "projectsAndSources")} />
{ data-source-columns={projectsLoading ? undefined : layoutPlan.columns} > {projectsLoading ? ( - <> +
+ {translate(overviewMessages, "loadingSourcesAnnouncement")}
- +
+ ) : recentSources.length === 0 ? ( +
+
) : ( recentSources.map((source, index) => (
}> = ({ pageData }) => { const listRef = useRef(null); - const [activeFilter, setActiveFilter] = useState("All Tasks"); + const [activeFilter, setActiveFilter] = useState("all"); + const { formatNumber, locale, translate, translatePlural } = useDashboardI18n(); + + const filterOptions = useMemo(() => [ + { value: "all", label: translate(overviewMessages, "filterAllTasks") }, + { value: "running", label: translate(overviewMessages, "filterRunning") }, + { value: "queued", label: translate(overviewMessages, "filterQueued") }, + { value: "completed", label: translate(overviewMessages, "filterCompleted") }, + ] as const, [translate]); - const handleFilterChange = (newFilter: TaskFilter) => { + const handleFilterChange = (newFilter: OverviewTaskFilter) => { setActiveFilter(newFilter); }; const handleClearFilter = () => { - setActiveFilter("All Tasks"); + setActiveFilter("all"); }; const { sprints, tasks, execution, selectedProject, isLoading } = pageData; @@ -36,13 +42,7 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType deriveActiveSprintIds(sprints), [sprints]); const activeTasks = useMemo(() => filterTasksToActiveSprints(tasks, activeSprintIds), [tasks, activeSprintIds]); - const filteredTasks = useMemo(() => activeTasks.filter(task => { - if (activeFilter === "All Tasks") return true; - if (activeFilter === "Running") return task.status === "in_progress"; - if (activeFilter === "Queued") return task.status === "pending"; - if (activeFilter === "Completed") return task.status === "completed"; - return true; - }), [activeTasks, activeFilter]); + const filteredTasks = useMemo(() => filterOverviewTasks(activeTasks, activeFilter), [activeTasks, activeFilter]); const visibleTasks = useProgressiveList(filteredTasks, { initialCount: DEFAULT_LIST_WINDOW, incrementCount: DEFAULT_LIST_WINDOW, @@ -87,26 +87,29 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType
-

Active Streams

+

{translate(overviewMessages, "activeStreams")}

- {filteredTasks.length} active + {translatePlural(overviewMessages, "activeCount", filteredTasks.length, { formattedCount: formatNumber(filteredTasks.length) })}
{/* Task rows */} -
+
{isLoading ? ( -
+
@@ -130,6 +133,7 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType streamActions.playStopTask(task)} /> @@ -137,10 +141,10 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType +
diff --git a/dashboard/src/v2/components/__tests__/SourcesGrid.test.tsx b/dashboard/src/v2/components/__tests__/SourcesGrid.test.tsx index ac519e23a2..f24e9b776a 100644 --- a/dashboard/src/v2/components/__tests__/SourcesGrid.test.tsx +++ b/dashboard/src/v2/components/__tests__/SourcesGrid.test.tsx @@ -6,6 +6,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { Source } from "../../types.js"; import { planSourcesGridLayout, SourcesGrid } from "../SourcesGrid.js"; import { ProjectDataContext } from "../../context/project-data.js"; +import { DashboardI18nProvider } from "../../i18n/index.js"; +import type { DashboardLocale } from "../../i18n/locales.js"; expect.extend(matchers); @@ -53,13 +55,14 @@ const createSource = (id: number, updatedAt: string): Source => ({ updatedAt, }); -const renderSourcesGrid = (projects: Source[]) => render( - render( + + render( deleteProject: vi.fn(), }} > - - , + + + , ); describe("SourcesGrid", () => { @@ -117,4 +121,23 @@ describe("SourcesGrid", () => { expect(grid.style.justifyContent).toBe("space-between"); expect(grid.style.gridTemplateColumns).toContain("calc((100% - 48px) / 3)"); }); + + it("localizes German source state and counts without changing project names", () => { + const source = { ...createSource(1234, "2026-01-06T00:00:00.000Z"), name: "Runtime Project Name", status: "running" } as Source; + renderSourcesGrid([source], "de"); + + expect(screen.getByRole("heading", { name: "Projekte & Quellen" })).toBeInTheDocument(); + expect(screen.getByText("Runtime Project Name")).toBeInTheDocument(); + expect(screen.getByText("Laufend")).toBeInTheDocument(); + expect(screen.getByText(`${new Intl.NumberFormat("de").format(1234)} offen`)).toBeInTheDocument(); + }); + + it("announces German loading and empty states", () => { + const loadingView = renderSourcesGrid([], "de", true); + expect(screen.getByRole("status", { name: "Projekte und Quellen werden geladen" })).toHaveAttribute("aria-busy", "true"); + loadingView.unmount(); + + renderSourcesGrid([], "de"); + expect(screen.getByRole("status")).toHaveTextContent("Keine Projekte oder Quellen"); + }); }); diff --git a/dashboard/src/v2/components/sprint/TaskReviewPanel.tsx b/dashboard/src/v2/components/sprint/TaskReviewPanel.tsx index ef1ae0f259..b41e8f2371 100644 --- a/dashboard/src/v2/components/sprint/TaskReviewPanel.tsx +++ b/dashboard/src/v2/components/sprint/TaskReviewPanel.tsx @@ -1,7 +1,5 @@ import { h } from "preact"; import { AlertCircle } from "lucide-preact"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; export interface TaskReviewPanelProps { task: { @@ -14,12 +12,11 @@ export interface TaskReviewPanelProps { } export function TaskReviewPanel({ task }: TaskReviewPanelProps) { - const { translate } = useOptionalDashboardI18n(); if (task.status !== "QA_REVIEW_FAILED") { return null; } - const errorReason = task.qa_review?.error_reason || translate(taskMessages, "reviewParsingFailed"); + const errorReason = task.qa_review?.error_reason || "Review parsing failed"; return (
-

{translate(taskMessages, "qaReviewFailed")}

+

QA Review Failed

{errorReason}

diff --git a/dashboard/src/v2/components/sprint/TaskStatusBadge.tsx b/dashboard/src/v2/components/sprint/TaskStatusBadge.tsx index 7e6001544c..80f05f937c 100644 --- a/dashboard/src/v2/components/sprint/TaskStatusBadge.tsx +++ b/dashboard/src/v2/components/sprint/TaskStatusBadge.tsx @@ -1,9 +1,5 @@ import { h } from "preact"; import { getStatusConfig } from "../../lib/status-labels"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import { getTaskStatusLabel } from "../../lib/tasks-constants.js"; -import type { TaskStatus } from "../../types.js"; export interface TaskStatusBadgeProps { status: string; @@ -11,17 +7,8 @@ export interface TaskStatusBadgeProps { } export function TaskStatusBadge({ status, className = "" }: TaskStatusBadgeProps) { - const { locale, translate } = useOptionalDashboardI18n(); const config = getStatusConfig(status); const Icon = config.icon; - const slotMatch = status.match(/^PENDING_cap_(\d+)_(\d+)$/i); - const label = slotMatch - ? translate(taskMessages, "waitingForSlot", { current: slotMatch[1], total: slotMatch[2] }) - : status === "pending" - ? translate(taskMessages, "pending") - : (["in_progress", "coding_completed", "completed", "QA_REVIEW_FAILED"] as string[]).includes(status) - ? getTaskStatusLabel(status as TaskStatus, locale) - : config.label; const variantStyles = { default: "bg-gray-100 text-gray-800 border-gray-200", @@ -37,7 +24,7 @@ export function TaskStatusBadge({ status, className = "" }: TaskStatusBadgeProps className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border ${variantStyles[config.variant]} max-w-full truncate ${className}`} > - {label} + {config.label}
); } diff --git a/dashboard/src/v2/components/sprints/SprintControls.tsx b/dashboard/src/v2/components/sprints/SprintControls.tsx index 08a379249d..351782be28 100644 --- a/dashboard/src/v2/components/sprints/SprintControls.tsx +++ b/dashboard/src/v2/components/sprints/SprintControls.tsx @@ -12,6 +12,25 @@ export interface SprintControlsProps { isPauseResumePending: boolean; onStartStop: () => void; onPauseResume: () => void; + labels?: SprintControlsLabels; +} + +export interface SprintControlsLabels { + pause: string; + resume: string; + start: string; + stop: string; + pending: (action: string) => string; + pendingLabel: (action: string, sprintName: string) => string; + actionLabel: (action: string, sprintName: string) => string; + waitForAction: string; + waitForActionTitle: string; + pauseUnavailable: string; + mustRunToPause: string; + resumeExecution: string; + pauseExecution: string; + stopExecution: string; + startExecution: string; } export const SprintControls: FunctionComponent = ({ @@ -22,18 +41,39 @@ export const SprintControls: FunctionComponent = ({ onStartStop, onPauseResume, sprintName, + labels, }) => { const { translate } = useDashboardI18n(); const resolvedSprintName = sprintName ?? translate(sprintsMessages, "sprint").toLocaleLowerCase(); - const getActionLabel = (action: string, pending: boolean): string => ( - sprintName - ? translate(sprintsMessages, pending ? "sprintActionPending" : "sprintAction", { action, name: sprintName }) - : translate(sprintsMessages, pending ? "sprintActionGenericPending" : "sprintActionGeneric", { action }) - ); + const resolvedLabels: SprintControlsLabels = labels ?? { + pause: translate(sprintsMessages, "pause"), + resume: translate(sprintsMessages, "resume"), + start: translate(sprintsMessages, "start"), + stop: translate(sprintsMessages, "stop"), + pending: (action) => translate(sprintsMessages, "primaryPending", { action }), + pendingLabel: (action, name) => translate( + sprintsMessages, + sprintName ? "sprintActionPending" : "sprintActionGenericPending", + sprintName ? { action, name } : { action }, + ), + actionLabel: (action, name) => translate( + sprintsMessages, + sprintName ? "sprintAction" : "sprintActionGeneric", + sprintName ? { action, name } : { action }, + ), + waitForAction: translate(sprintsMessages, "waitCurrentAction"), + waitForActionTitle: translate(sprintsMessages, "waitCurrentActionNoPeriod"), + pauseUnavailable: translate(sprintsMessages, "pauseAfterStart"), + mustRunToPause: translate(sprintsMessages, "sprintMustRunToPause"), + resumeExecution: translate(sprintsMessages, "resumeExecution"), + pauseExecution: translate(sprintsMessages, "pauseExecution"), + stopExecution: translate(sprintsMessages, "stopExecution"), + startExecution: translate(sprintsMessages, "startExecution"), + }; const interactionTokens = useInteractionTokens(); const canPauseResume = isActive || isPaused; - const pauseResumeLabel = translate(sprintsMessages, isPaused ? "resume" : "pause"); - const startStopLabel = translate(sprintsMessages, isActive ? "stop" : "start"); + const pauseResumeLabel = isPaused ? resolvedLabels.resume : resolvedLabels.pause; + const startStopLabel = isActive ? resolvedLabels.stop : resolvedLabels.start; const isAnyPending = isPauseResumePending || isStartStopPending; const controlFeedbackStyle = { transitionDuration: interactionTokens.controlFeedback.duration, @@ -44,14 +84,14 @@ export const SprintControls: FunctionComponent = ({ transitionTimingFunction: interactionTokens.asyncFeedback.ease, }; const busyLabel = isPauseResumePending - ? translate(sprintsMessages, "primaryPending", { action: pauseResumeLabel }) + ? resolvedLabels.pending(pauseResumeLabel) : isStartStopPending - ? translate(sprintsMessages, "primaryPending", { action: startStopLabel }) + ? resolvedLabels.pending(startStopLabel) : null; const disabledReason = isAnyPending - ? translate(sprintsMessages, "waitCurrentAction") + ? resolvedLabels.waitForAction : !canPauseResume - ? translate(sprintsMessages, "pauseAfterStart") + ? resolvedLabels.pauseUnavailable : null; const reasonId = `sprint-controls-${resolvedSprintName.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-reason`; const handlePauseResume = () => { @@ -74,22 +114,22 @@ export const SprintControls: FunctionComponent = ({ onClick={handlePauseResume} aria-label={ isPauseResumePending - ? getActionLabel(pauseResumeLabel, true) + ? resolvedLabels.pendingLabel(pauseResumeLabel, resolvedSprintName) : isPaused - ? getActionLabel(translate(sprintsMessages, "resume"), false) - : getActionLabel(translate(sprintsMessages, "pause"), false) + ? resolvedLabels.actionLabel(resolvedLabels.resume, resolvedSprintName) + : resolvedLabels.actionLabel(resolvedLabels.pause, resolvedSprintName) } aria-busy={isPauseResumePending ? "true" : undefined} aria-describedby={disabledReason ? reasonId : undefined} disabled={!canPauseResume || isAnyPending} title={ isPauseResumePending || isStartStopPending - ? translate(sprintsMessages, "waitCurrentActionNoPeriod") + ? resolvedLabels.waitForActionTitle : !canPauseResume - ? translate(sprintsMessages, "sprintMustRunToPause") + ? resolvedLabels.mustRunToPause : isPaused - ? translate(sprintsMessages, "resumeExecution") - : translate(sprintsMessages, "pauseExecution") + ? resolvedLabels.resumeExecution + : resolvedLabels.pauseExecution } className={`inline-flex min-h-8 min-w-[6.75rem] flex-1 flex-nowrap items-center justify-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-bold leading-tight no-underline decoration-transparent transition-colors hover:no-underline focus:no-underline focus-visible:ring-2 focus-visible:ring-signal-500/30 sm:flex-none ${ isPaused @@ -115,20 +155,20 @@ export const SprintControls: FunctionComponent = ({ onClick={handleStartStop} aria-label={ isStartStopPending - ? getActionLabel(startStopLabel, true) + ? resolvedLabels.pendingLabel(startStopLabel, resolvedSprintName) : isActive - ? getActionLabel(translate(sprintsMessages, "stop"), false) - : getActionLabel(translate(sprintsMessages, "start"), false) + ? resolvedLabels.actionLabel(resolvedLabels.stop, resolvedSprintName) + : resolvedLabels.actionLabel(resolvedLabels.start, resolvedSprintName) } aria-busy={isStartStopPending ? "true" : undefined} aria-describedby={disabledReason ? reasonId : undefined} disabled={isAnyPending} title={ isStartStopPending || isPauseResumePending - ? translate(sprintsMessages, "waitCurrentActionNoPeriod") + ? resolvedLabels.waitForActionTitle : isActive - ? translate(sprintsMessages, "stopExecution") - : translate(sprintsMessages, "startExecution") + ? resolvedLabels.stopExecution + : resolvedLabels.startExecution } className={`inline-flex min-h-8 min-w-[6.75rem] flex-1 flex-nowrap items-center justify-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-bold leading-tight no-underline decoration-transparent transition-colors hover:no-underline focus:no-underline focus-visible:ring-2 focus-visible:ring-signal-500/30 sm:flex-none ${ isActive @@ -154,7 +194,7 @@ export const SprintControls: FunctionComponent = ({ aria-live="polite" className={busyLabel ? "basis-full text-left text-[11px] font-bold leading-4 text-signal-600 dark:text-signal-300" : "sr-only"} > - {busyLabel ? `${busyLabel}. ${translate(sprintsMessages, "waitCurrentAction")}` : disabledReason ?? ""} + {busyLabel ? `${busyLabel}. ${resolvedLabels.waitForAction}` : disabledReason ?? ""} ); diff --git a/dashboard/src/v2/components/tasks/DependencyStatusIndicators.tsx b/dashboard/src/v2/components/tasks/DependencyStatusIndicators.tsx index 3f130f2e7e..6e0b00a8cb 100644 --- a/dashboard/src/v2/components/tasks/DependencyStatusIndicators.tsx +++ b/dashboard/src/v2/components/tasks/DependencyStatusIndicators.tsx @@ -3,9 +3,30 @@ import { memo } from "preact/compat"; import { ArrowRight } from "lucide-preact"; import { getDependencyPresentation, type DependencyIndicator } from "../../lib/tasks/task-card-view-model.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import type { DashboardTranslate } from "../../i18n/context.js"; + +function getDependencyStatusCopy(dep: DependencyIndicator): string { + if (dep.stateLabel) { + return dep.stateLabel; + } + + if (dep.isKnown === false || dep.title.startsWith("Unknown Task")) { + return "Unknown"; + } + + switch (dep.status) { + case "completed": + return "Resolved"; + case "coding_completed": + return "Ready for QA"; + case "in_progress": + return "In progress"; + case "QA_REVIEW_FAILED": + return "QA failed"; + case "pending": + default: + return "Blocked"; + } +} function getDependencyState(dep: DependencyIndicator): "unknown" | "resolved" | "qa_failed" | "in_progress" | "blocked" { if (dep.isKnown === false || dep.title.startsWith("Unknown Task")) { @@ -50,46 +71,25 @@ function getDependencyToneClass(dep: DependencyIndicator): string { } } -function getDependencyStatusText( - status: DependencyIndicator["status"], - locale: "en" | "de", - translate: DashboardTranslate, -): string { - if (locale === "en") { - return status.replace(/_/g, " "); - } - const key = status === "pending" - ? "pending" - : status === "in_progress" - ? "inProgressLower" - : status === "coding_completed" - ? "codingCompleted" - : status === "QA_REVIEW_FAILED" - ? "qaFailedLower" - : "completed"; - return translate(taskMessages, key).toLocaleLowerCase(locale); -} - export const DependencyStatusIndicators: FunctionComponent<{ indicators: DependencyIndicator[]; }> = memo(({ indicators }) => { const interactionTokens = useInteractionTokens(); - const { locale, translate, translatePlural, formatNumber } = useOptionalDashboardI18n(); if (!indicators || indicators.length === 0) return null; const blockerCount = indicators.filter(isDependencyBlocking).length; const summary = blockerCount === 0 - ? translate(taskMessages, "dependenciesResolved", { count: formatNumber(indicators.length) }) - : translatePlural(taskMessages, "dependenciesBlocked", blockerCount, { count: formatNumber(blockerCount) }); + ? `Dependencies resolved: ${indicators.length} clear` + : `Blocked: ${blockerCount} ${blockerCount === 1 ? "dependency needs" : "dependencies need"} completion`; const announcement = blockerCount === 0 - ? translatePlural(taskMessages, "dependencyResolvedAnnouncement", indicators.length, { count: formatNumber(indicators.length) }) + ? `Dependency blockers resolved. ${indicators.length} ${indicators.length === 1 ? "dependency is" : "dependencies are"} clear.` : `${summary}.`; return (
{indicators.map((dep) => { - const statusText = getDependencyStatusText(dep.status, locale, translate); - const presentation = getDependencyPresentation(dep.status, dep.isKnown !== false && !dep.title.startsWith("Unknown Task"), locale); - const statusCopy = dep.stateLabel ?? presentation.stateLabel ?? translate(taskMessages, "unknown"); + const statusText = dep.status.replace(/_/g, ' '); + const statusCopy = getDependencyStatusCopy(dep); const dependencyState = getDependencyState(dep); - const blockingCopy = translate(taskMessages, isDependencyBlocking(dep) ? "blockingDependency" : "resolvedDependency"); - const blockingBadge = translate(taskMessages, isDependencyBlocking(dep) ? "blocking" : "clear"); - const stateDescription = dep.stateDescription ?? presentation.stateDescription; - const accessibleDescription = translate(taskMessages, "dependsOnAccessible", { - id: dep.id, - state: statusCopy.toLocaleLowerCase(locale), - blocking: blockingCopy, - description: stateDescription ?? "", - status: statusText, - title: dep.title, - }); + const blockingCopy = isDependencyBlocking(dep) ? "Blocking dependency" : "Resolved dependency"; + const blockingBadge = isDependencyBlocking(dep) ? "Blocking" : "Clear"; + const stateDescription = dep.stateDescription ?? getDependencyPresentation(dep.status, dep.isKnown !== false && !dep.title.startsWith("Unknown Task")).stateDescription; const containerClass = getDependencyToneClass(dep); return ( @@ -138,10 +129,10 @@ export const DependencyStatusIndicators: FunctionComponent<{ className={`flex min-h-7 max-w-full items-center gap-1.5 rounded-lg border px-2 py-1 text-[9px] font-bold uppercase tracking-[0.12em] transition-colors motion-reduce:transition-none ${containerClass}`} data-dependency-state={dependencyState} data-blocking={isDependencyBlocking(dep) ? "true" : "false"} - title={translate(taskMessages, "dependsOnTitle", { title: dep.title, state: statusCopy, status: statusText })} - aria-label={accessibleDescription} + title={`Depends on ${dep.title} (${statusCopy}; ${statusText})`} + aria-label={`Depends on task ${dep.id}, ${statusCopy.toLowerCase()}. ${blockingCopy}. ${stateDescription}. Status: ${statusText}. Title: ${dep.title}`} > - {accessibleDescription} + Depends on task {dep.id}, {statusCopy.toLowerCase()}. {blockingCopy}. {stateDescription}. Status: {statusText}. Title: {dep.title}
@@ -219,15 +205,15 @@ export const KanbanTaskCard: FunctionComponent<{ {agentPresetName && ( - {translate(taskMessages, "agent")}: {agentPresetName} + Agent: {agentPresetName} )} {sessionState && ( - {translate(taskMessages, "sessionState")}: {sessionState} + Session state: {sessionState} )} {sessionId && ( - {translate(taskMessages, "sessionId")}: {sessionId} + Session ID: {sessionId} )} {task.latestReview ? ( @@ -235,7 +221,7 @@ export const KanbanTaskCard: FunctionComponent<{ ) : ( {qaNoReviewLabel} @@ -248,14 +234,14 @@ export const KanbanTaskCard: FunctionComponent<{ {dependencyIndicators.length > 0 && ( 0 ? "border-status-amber/25 bg-status-amber/[0.08] text-status-amber" : "border-status-green/20 bg-status-green/[0.08] text-status-green"}`} - aria-label={translate(taskMessages, blockerCount > 0 ? "dependenciesBlockedAria" : "dependenciesClearAria", { label: dependencyActionLabel })} + aria-label={blockerCount > 0 ? `${dependencyActionLabel}. Dependencies blocked.` : `${dependencyActionLabel}. No dependency blockers.`} > {dependencyActionLabel} )} {task.isOptimistic && ( - {viewModel.optimisticSavingLabel ?? translate(taskMessages, "saving")} + {viewModel.optimisticSavingLabel ?? "Saving"} )}
@@ -274,7 +260,7 @@ export const KanbanTaskCard: FunctionComponent<{ {task.assignee[0]}
- {translate(taskMessages, "assignee")}: {task.assignee} + Assignee: {task.assignee}
@@ -285,16 +271,16 @@ export const KanbanTaskCard: FunctionComponent<{
- {liveStartedAt ? `· ${formatTimeAgo(liveStartedAt, Date.now(), locale)}` : humanizedCreatedAt} + {liveStartedAt ? `· ${formatTimeAgo(liveStartedAt)}` : humanizedCreatedAt}
-
+
{unavailableActionSummaryText && (
diff --git a/dashboard/src/v2/components/tasks/SelfReflectionRatingBadge.tsx b/dashboard/src/v2/components/tasks/SelfReflectionRatingBadge.tsx index 40c04918b6..e9e4a56cb2 100644 --- a/dashboard/src/v2/components/tasks/SelfReflectionRatingBadge.tsx +++ b/dashboard/src/v2/components/tasks/SelfReflectionRatingBadge.tsx @@ -8,8 +8,6 @@ import { type SelfReflectionStarState, } from "../../lib/tasks/self-reflection-rating.js"; import { calculatePosition, type Alignment, type Position } from "../../lib/positioning/index.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; interface SelfReflectionRatingBadgeProps { rating: TaskSelfReflectionRating | null | undefined; @@ -24,8 +22,7 @@ export const SelfReflectionRatingBadge: FunctionComponent { - const { locale, translate } = useOptionalDashboardI18n(); - const viewModel = buildSelfReflectionRatingViewModel(rating, locale); + const viewModel = buildSelfReflectionRatingViewModel(rating); const [overlayId] = useState(() => `self-reflection-rating-${Math.random().toString(36).slice(2, 10)}`); const [isOpen, setIsOpen] = useState(false); const triggerRef = useRef(null); @@ -152,7 +149,7 @@ export const SelfReflectionRatingBadge: FunctionComponent
{viewModel.overallRatingLabel} @@ -191,7 +188,7 @@ export const SelfReflectionRatingBadge: FunctionComponent ) : (

- {translate(taskMessages, "noSectionRatings")} + No section ratings recorded.

)}
diff --git a/dashboard/src/v2/components/tasks/TaskBoardColumns.tsx b/dashboard/src/v2/components/tasks/TaskBoardColumns.tsx index 1bce6a7f88..619a29f5d5 100644 --- a/dashboard/src/v2/components/tasks/TaskBoardColumns.tsx +++ b/dashboard/src/v2/components/tasks/TaskBoardColumns.tsx @@ -4,15 +4,12 @@ import type { RefObject } from "preact"; import type { AgentAvatarConfig, AgentPreset, Task, TaskStatus } from "../../types.js"; import type { TaskBoardState } from "../../lib/task-board-state.js"; import type { TaskCardViewModel } from "../../lib/tasks/task-card-view-model.js"; -import { STATUS_CFG, getTaskStatusLabel } from "../../lib/tasks-constants.js"; +import { STATUS_CFG } from "../../lib/tasks-constants.js"; import { getTaskDropFeedback } from "../../lib/tasks/task-board-actions.js"; import { SkeletonCard, SkeletonLoader } from "../layout/SkeletonLoader.js"; import { KanbanTaskCard } from "./KanbanTaskCard.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; const ColumnHeader: FunctionComponent<{ status: TaskStatus; count: number }> = memo(({ status, count }) => { - const { locale, translatePlural, formatNumber } = useOptionalDashboardI18n(); const cfg = STATUS_CFG[status]; const Icon = cfg.icon; const headingId = `task-lane-heading-${status}`; @@ -21,11 +18,11 @@ const ColumnHeader: FunctionComponent<{ status: TaskStatus; count: number }> = m
-

{getTaskStatusLabel(status, locale)}

+

{cfg.label}

- - {translatePlural(taskMessages, "taskCount", count, { count: formatNumber(count) })} + + {count} {count === 1 ? "task" : "tasks"}
); @@ -155,9 +152,7 @@ const TaskBoardColumnsComponent: FunctionComponent = ({ onDragEnd, onEditTask, onDeleteTask, -}) => { - const { locale, translate, translatePlural, formatNumber } = useOptionalDashboardI18n(); - return ( +}) => (
= ({ >

- {translate(taskMessages, "laneSummary", { - status: getTaskStatusLabel(status, locale), - tasks: translatePlural(taskMessages, "taskCount", count, { count: formatNumber(count) }), - })} + {STATUS_CFG[status].label} lane contains {count} {count === 1 ? "task" : "tasks"} after current filters.

= ({ isDragging: draggedTaskId !== null, targetLane: status, currentStatus: draggedTaskId ? allTasks.find((task) => task.recordId === draggedTaskId)?.status : undefined, - locale, })}

{showSkeletons && (
- {translate(taskMessages, "loadingLane", { status: getTaskStatusLabel(status, locale).toLocaleLowerCase(locale) })} + Loading {STATUS_CFG[status].label.toLowerCase()} tasks.
)} = ({ > {!loading && columnTasks.length === 0 ? (
- {translate(taskMessages, "emptyLane", { - status: getTaskStatusLabel(status, locale).toLocaleLowerCase(locale), - scope: translate(taskMessages, statusFilter !== "all" || priorityFilter !== "all" ? "matchingFilters" : taskScopeSprintId ? "inThisSprint" : "inThisProject"), - })} + No {status.replace("_", " ")} tasks +
+ {statusFilter !== "all" || priorityFilter !== "all" ? "matching current filters" : taskScopeSprintId ? "in this sprint" : "in this project"}.
) : !loading ? (
@@ -273,7 +263,6 @@ const TaskBoardColumnsComponent: FunctionComponent = ({
))} - ); -}; +); export const TaskBoardColumns = memo(TaskBoardColumnsComponent, areTaskBoardColumnsPropsEqual); diff --git a/dashboard/src/v2/components/tasks/TaskBoardFilters.tsx b/dashboard/src/v2/components/tasks/TaskBoardFilters.tsx index 47e7b79672..00880178b1 100644 --- a/dashboard/src/v2/components/tasks/TaskBoardFilters.tsx +++ b/dashboard/src/v2/components/tasks/TaskBoardFilters.tsx @@ -5,9 +5,6 @@ import { FilterStrip } from "../ui/FilterStrip.js"; import { ListWindowSelector } from "../ui/ListWindowSelector.js"; import { TaskBoardSprintSelector } from "./TaskBoardSprintSelector.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import { getTaskPriorityLabel, getTaskStatusLabel } from "../../lib/tasks-constants.js"; export type TaskBoardStatusFilter = "all" | TaskStatus; export type TaskBoardPriorityFilter = "all" | TaskPriority; @@ -26,22 +23,22 @@ export interface TaskBoardFiltersProps { onListWindowChange: (value: ListWindowOption) => void; } -function getStatusFilterLabel(filter: TaskBoardStatusFilter, locale: "en" | "de"): string { +function getStatusFilterLabel(filter: TaskBoardStatusFilter): string { switch (filter) { - case "in_progress": return getTaskStatusLabel(filter, locale); - case "pending": return getTaskStatusLabel(filter, locale); - case "completed": return getTaskStatusLabel(filter, locale); + case "in_progress": return "Running"; + case "pending": return "Queued"; + case "completed": return "Done"; case "all": default: return "All"; } } -function getPriorityFilterLabel(filter: TaskBoardPriorityFilter, locale: "en" | "de"): string { +function getPriorityFilterLabel(filter: TaskBoardPriorityFilter): string { switch (filter) { - case "critical": return getTaskPriorityLabel(filter, locale); - case "high": return getTaskPriorityLabel(filter, locale); - case "medium": return getTaskPriorityLabel(filter, locale); - case "low": return getTaskPriorityLabel(filter, locale); + case "critical": return "Critical"; + case "high": return "High"; + case "medium": return "Medium"; + case "low": return "Low"; case "all": default: return "Any Priority"; } @@ -61,13 +58,7 @@ export const TaskBoardFilters: FunctionComponent = ({ onListWindowChange, }) => { const interactionTokens = useInteractionTokens(); - const { locale, translate, formatNumber } = useOptionalDashboardI18n(); - const statusLabel = statusFilter === "all" ? translate(taskMessages, "all") : getStatusFilterLabel(statusFilter, locale); - const priorityLabel = priorityFilter === "all" ? translate(taskMessages, "anyPriority") : getPriorityFilterLabel(priorityFilter, locale); - const windowLabel = typeof listWindow === "string" - ? translate(taskMessages, "allTasksWindow") - : translate(taskMessages, "tasksPerLaneWindow", { count: formatNumber(listWindow) }); - const filterAnnouncement = translate(taskMessages, "filterAnnouncement", { status: statusLabel, priority: priorityLabel, window: windowLabel }); + const filterAnnouncement = `Task filters changed. Status ${getStatusFilterLabel(statusFilter)}. Priority ${getPriorityFilterLabel(priorityFilter)}. Showing ${listWindow === "All" ? "all tasks" : `${listWindow} tasks per lane`}.`; return (
= ({
= ({ diff --git a/dashboard/src/v2/components/tasks/TaskBoardSprintSelector.tsx b/dashboard/src/v2/components/tasks/TaskBoardSprintSelector.tsx index 0e119b1e2b..9336a0fa4a 100644 --- a/dashboard/src/v2/components/tasks/TaskBoardSprintSelector.tsx +++ b/dashboard/src/v2/components/tasks/TaskBoardSprintSelector.tsx @@ -6,10 +6,7 @@ import type { Sprint } from "../../types.js"; import { formatSprintDisplay } from "../../lib/format-sprint.js"; import { buildTaskBoardSprintScopeState } from "../../lib/tasks/task-board-view-model.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; -import { clampSprintCompletion } from "../../lib/sprint-progress-display.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import { formatTaskSprintDateRange } from "../../lib/tasks/task-presentation.js"; +import { clampSprintCompletion, formatSprintCompletion } from "../../lib/sprint-progress-display.js"; export interface TaskBoardSprintSelectorProps { sprints: Sprint[]; @@ -32,26 +29,24 @@ export const TaskBoardSprintSelector: FunctionComponent(null); const optionRefs = useRef>([]); const interactionTokens = useInteractionTokens(); - const { locale, translate, translatePlural, formatNumber } = useOptionalDashboardI18n(); const listboxId = "tasks-sprint-selector-listbox"; const statusId = "tasks-sprint-selector-status"; const selected = selectedId ? sprints.find((sprint: Sprint) => sprint.id === selectedId) : null; const selectedLabel = selected ? formatSprintDisplay(selected, sprintKeyPrefix) : null; const selectedAnnouncement = selectedLabel - ? translate(taskMessages, "selectedScopeSprint", { scope: selectedLabel }) - : translate(taskMessages, "selectedScopeAll"); + ? `Selected sprint scope changed to ${selectedLabel}.` + : "Selected sprint scope changed to All Sprints."; const scopeState = buildTaskBoardSprintScopeState({ sprints, selectedSprintId: selectedId, selectedSprintLabel: selectedLabel, loading, - locale, }); const options = useMemo(() => [ { id: null as string | null, - label: translate(taskMessages, "allSprints"), - description: translate(taskMessages, "projectWideTaskScope"), + label: "All Sprints", + description: "Project-wide task scope", sprint: null as Sprint | null, completion: 0, }, @@ -60,16 +55,12 @@ export const TaskBoardSprintSelector: FunctionComponent option.id === selectedId)); @@ -198,7 +189,7 @@ export const TaskBoardSprintSelector: FunctionComponent
- {open ? translate(taskMessages, "scopeListOpen", { description: scopeState.description }) : `${selectedAnnouncement} ${scopeState.description}`} + {open ? `Sprint scope list open. ${scopeState.description}` : `${selectedAnnouncement} ${scopeState.description}`}
@@ -244,7 +235,7 @@ export const TaskBoardSprintSelector: FunctionComponent
- {translate(taskMessages, "allSprints")} - {translate(taskMessages, "projectWideScope")} + All Sprints + Project-wide scope
- {!selectedId && {translate(taskMessages, "selected")}} + {!selectedId && Selected}
@@ -282,12 +273,12 @@ export const TaskBoardSprintSelector: FunctionComponent {loading && (
- {translate(taskMessages, "loadingSprintScopes")} + Loading sprint scopes
)} {!loading && sprints.length === 0 && (
- {translate(taskMessages, "noSprintScopes")} + No sprints available. All Sprints remains selected until a sprint is created.
)} {sprints.map((sprint, sprintIndex) => { @@ -326,13 +317,13 @@ export const TaskBoardSprintSelector: FunctionComponent
- {formatTaskSprintDateRange(sprint.startDate, sprint.endDate, locale)} + {sprint.date} {options[index].description}
- {isActive && {translate(taskMessages, "selected")}} - {formatNumber(sprint.tasksCount)} + {isActive && Selected} + {sprint.tasksCount}
diff --git a/dashboard/src/v2/components/tasks/TaskExecutionMeta.tsx b/dashboard/src/v2/components/tasks/TaskExecutionMeta.tsx index 514c0422d9..5fac8298c6 100644 --- a/dashboard/src/v2/components/tasks/TaskExecutionMeta.tsx +++ b/dashboard/src/v2/components/tasks/TaskExecutionMeta.tsx @@ -3,9 +3,6 @@ import { memo } from "preact/compat"; import { Clock, Settings, Cpu, User } from "lucide-preact"; import type { TaskExecutorType } from "../../types.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import { formatTaskTimeState } from "../../lib/tasks/task-presentation.js"; export interface TaskExecutionMetaProps { time?: string; @@ -21,7 +18,6 @@ export const TaskExecutionMeta: FunctionComponent = memo className = "", }) => { const interactionTokens = useInteractionTokens(); - const { locale, translate } = useOptionalDashboardI18n(); const getExecutorIcon = () => { switch (executorType) { case "docker_cli": @@ -43,19 +39,17 @@ export const TaskExecutionMeta: FunctionComponent = memo } }; - const durationLabel = time ? formatTaskTimeState(time, locale) : translate(taskMessages, "notStarted"); + const durationLabel = time || "Not started"; const executorLabel = getExecutorLabel(); - const modeLabel = executionMode || translate(taskMessages, "standard"); - const runtimeAvailabilityLabel = time - ? translate(taskMessages, "runtimeAvailable", { duration: durationLabel }) - : translate(taskMessages, "runtimeNotStarted"); + const modeLabel = executionMode || "Standard"; + const runtimeAvailabilityLabel = time ? `Runtime duration available: ${durationLabel}` : "Runtime not started."; const showExecutorVisibly = executorType !== "auto" && executorType !== undefined; return (
= memo data-motion-selection="selectionMovement" > - {translate(taskMessages, "executionAnnouncement", { runtime: runtimeAvailabilityLabel, executor: executorLabel, mode: modeLabel })} + {runtimeAvailabilityLabel} Executor {executorLabel}. Mode {modeLabel}.
{getExecutorIcon()} - {translate(taskMessages, "executor")} + Executor {executorLabel}
diff --git a/dashboard/src/v2/components/tasks/__tests__/DependencyStatusIndicators.test.tsx b/dashboard/src/v2/components/tasks/__tests__/DependencyStatusIndicators.test.tsx index ae208ce264..9d2530e0ef 100644 --- a/dashboard/src/v2/components/tasks/__tests__/DependencyStatusIndicators.test.tsx +++ b/dashboard/src/v2/components/tasks/__tests__/DependencyStatusIndicators.test.tsx @@ -3,7 +3,6 @@ import { describe, it, expect, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/preact"; import * as matchers from "@testing-library/jest-dom/matchers"; import { DependencyStatusIndicators } from "../DependencyStatusIndicators.js"; -import { DashboardI18nProvider } from "../../../i18n/context.js"; expect.extend(matchers); @@ -96,21 +95,4 @@ describe("DependencyStatusIndicators", () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); - - it("announces German dependency validation without translating task records", () => { - const { getByRole, getByText } = render( - - - , - ); - - expect(getByRole("list")).toHaveAccessibleName(/Blockiert: 2 Abhängigkeiten müssen abgeschlossen werden.*Aufgabenabhängigkeiten/i); - expect(getByRole("listitem", { name: /Abhängig von Aufgabe TASK_KEY_9/i })).toHaveTextContent("Keep dependency title"); - expect(getByText("QA provider output")).toBeInTheDocument(); - }); }); diff --git a/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx b/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx index 42164e2fcb..e18258af32 100644 --- a/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx +++ b/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx @@ -2,15 +2,14 @@ /// import { readFileSync } from "node:fs"; import { describe, it, expect, afterEach, vi } from "vitest"; -import { render, cleanup, fireEvent, waitFor, within, screen } from "@testing-library/preact"; +import { render, cleanup, fireEvent, waitFor, within } from "@testing-library/preact"; import * as matchers from "@testing-library/jest-dom/matchers"; import userEvent from "@testing-library/user-event"; import gsap from "gsap"; import { KanbanTaskCard } from "../KanbanTaskCard.js"; -import { buildTaskCardViewModel, type TaskCardViewModel } from "../../../lib/tasks/task-card-view-model.js"; +import type { TaskCardViewModel } from "../../../lib/tasks/task-card-view-model.js"; import type { CiStatusPresentation } from "../../../lib/ci-status-presentation.js"; import type { TaskSelfReflectionRating } from "../../../../../../src/contracts/task-self-reflection-types.js"; -import { DashboardI18nProvider } from "../../../i18n/context.js"; expect.extend(matchers); @@ -817,37 +816,4 @@ describe("KanbanTaskCard Integration", () => { expect(actionsContainer).toHaveAttribute("aria-label", "Actions for task TASK-123"); }); - it("renders German card actions and delete confirmation without translating task content", async () => { - const task = { - ...mockViewModel.task, - id: "TASK_KEY_DE_1", - title: "Keep persisted English title", - status: "in_progress" as const, - priority: "critical" as const, - promptMarkdown: "## Keep this prompt verbatim", - sprintId: "sprint-de", - dependsOnTaskIds: [], - }; - const viewModel = buildTaskCardViewModel(task, new Map(), undefined, { locale: "de" }); - const onDelete = vi.fn(); - render( - - - , - ); - - expect(screen.getByText("Keep persisted English title")).toBeInTheDocument(); - expect(screen.getByText("In Bearbeitung")).toBeInTheDocument(); - const deleteButton = screen.getByRole("button", { name: /Aufgabe TASK_KEY_DE_1 löschen/i }); - expect(deleteButton).toHaveTextContent("Löschen"); - fireEvent.click(deleteButton); - - await waitFor(() => expect(mockRequestConfirm).toHaveBeenCalledWith(expect.objectContaining({ - title: "Aufgabe löschen", - confirmLabel: "Aufgabe löschen", - body: expect.stringContaining("Keep persisted English title"), - }))); - expect(onDelete).toHaveBeenCalledWith(task); - }); - }); diff --git a/dashboard/src/v2/components/tasks/__tests__/TaskExecutionMeta.test.tsx b/dashboard/src/v2/components/tasks/__tests__/TaskExecutionMeta.test.tsx index 2f2bf16240..3df2ff7640 100644 --- a/dashboard/src/v2/components/tasks/__tests__/TaskExecutionMeta.test.tsx +++ b/dashboard/src/v2/components/tasks/__tests__/TaskExecutionMeta.test.tsx @@ -3,7 +3,6 @@ import { describe, it, expect, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/preact"; import * as matchers from "@testing-library/jest-dom/matchers"; import { TaskExecutionMeta } from "../TaskExecutionMeta.js"; -import { DashboardI18nProvider } from "../../../i18n/context.js"; expect.extend(matchers); @@ -59,22 +58,6 @@ describe("TaskExecutionMeta", () => { expect(getByText("Standard")).toBeTruthy(); }); - it("localizes known German task-time states but preserves arbitrary runtime values", () => { - const { rerender } = render( - - - , - ); - - expect(document.body).toHaveTextContent("Prüfung"); - rerender( - - - , - ); - expect(document.body).toHaveTextContent("Runtime checkpoint: Review"); - }); - it("applies custom className", () => { const { container } = render(); diff --git a/dashboard/src/v2/components/ui/CellActions.tsx b/dashboard/src/v2/components/ui/CellActions.tsx index f95546669d..552e21ca2b 100644 --- a/dashboard/src/v2/components/ui/CellActions.tsx +++ b/dashboard/src/v2/components/ui/CellActions.tsx @@ -15,6 +15,8 @@ interface CellActionsProps { onPrimaryAction?: () => void | Promise; onSprintsClick?: () => void | Promise; onSettingsClick?: () => void | Promise; + primaryLabel?: string; + settingsLabel?: string; } /** @@ -29,6 +31,8 @@ export const CellActions: FunctionComponent = ({ onPrimaryAction, onSprintsClick, onSettingsClick, + primaryLabel, + settingsLabel = "Settings", }) => { const { feedback: primaryFeedback, setPending: setPrimaryPending, setSuccess: setPrimarySuccess, setError: setPrimaryError } = useActionFeedback(1500); const tokens = useInteractionTokens(); @@ -60,7 +64,7 @@ export const CellActions: FunctionComponent = ({ )}
diff --git a/dashboard/src/v2/components/ui/RerunTaskModal.tsx b/dashboard/src/v2/components/ui/RerunTaskModal.tsx index 7f5e01c367..e4b7f4808b 100644 --- a/dashboard/src/v2/components/ui/RerunTaskModal.tsx +++ b/dashboard/src/v2/components/ui/RerunTaskModal.tsx @@ -14,8 +14,6 @@ import { import { ProviderBrandIcon } from "../providers/ProviderBrandIcon.js"; import { ActionFeedbackRegion } from "./ActionFeedbackRegion.js"; import { Modal } from "./Modal.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; interface RerunTaskModalProps { task: Subtask; @@ -35,7 +33,6 @@ export const RerunTaskModal: FunctionComponent = ({ onClose, onConfirm, }) => { - const { translate, translatePlural, formatNumber } = useOptionalDashboardI18n(); const [providerConfigId, setProviderConfigId] = useState(""); const [clearWorktree, setClearWorktree] = useState(false); const [resetDependents, setResetDependents] = useState(false); @@ -64,7 +61,7 @@ export const RerunTaskModal: FunctionComponent = ({ }, []); const providerOptions = useMemo(() => { - const base = [{ value: "", label: translate(taskMessages, "autoCurrentSetting") }]; + const base = [{ value: "", label: "Auto (use current setting)" }]; if (!systemSettings) { return base; } @@ -78,7 +75,7 @@ export const RerunTaskModal: FunctionComponent = ({ })); return [...base, ...available]; - }, [systemSettings, translate]); + }, [systemSettings]); const [model, setModel] = useState(""); const selectedProvider = useMemo(() => ( @@ -155,7 +152,7 @@ export const RerunTaskModal: FunctionComponent = ({ const handleSubmit = async () => { if (isSubmitting) return; setIsSubmitting(true); - setPending(translate(taskMessages, "preparingRerun")); + setPending("Preparing task rerun..."); try { await onConfirm({ provider: selectedProvider?.provider, @@ -166,12 +163,12 @@ export const RerunTaskModal: FunctionComponent = ({ undoMerge: taskAlreadyMerged && undoMerge, }); setIsSubmitting(false); - setSuccess(translate(taskMessages, "rerunStarted"), { autoDismiss: false }); + setSuccess("Task rerun started.", { autoDismiss: false }); window.setTimeout(() => onClose(), 700); } catch (err) { setIsSubmitting(false); const message = err instanceof Error ? err.message : String(err); - setError(message || translate(taskMessages, "rerunFailed"), { retryAction: handleSubmit, retryLabel: translate(taskMessages, "retry"), autoDismiss: false }); + setError(message || "Failed to rerun task.", { retryAction: handleSubmit, retryLabel: "Retry", autoDismiss: false }); } }; @@ -201,7 +198,7 @@ export const RerunTaskModal: FunctionComponent = ({

- {translate(taskMessages, "rerunTask")} + Rerun Task

#{task.id}

@@ -209,7 +206,7 @@ export const RerunTaskModal: FunctionComponent = ({ diff --git a/dashboard/src/v2/components/ui/SectionHeader.tsx b/dashboard/src/v2/components/ui/SectionHeader.tsx index a6affd9497..89e19110b4 100644 --- a/dashboard/src/v2/components/ui/SectionHeader.tsx +++ b/dashboard/src/v2/components/ui/SectionHeader.tsx @@ -19,13 +19,13 @@ export const SectionHeader: FunctionComponent = ({ }) => (
-

+

-

- {icon} + +

+ {title} -

+
); diff --git a/dashboard/src/v2/components/ui/SourceCell.tsx b/dashboard/src/v2/components/ui/SourceCell.tsx index adb1597510..9663104834 100644 --- a/dashboard/src/v2/components/ui/SourceCell.tsx +++ b/dashboard/src/v2/components/ui/SourceCell.tsx @@ -6,12 +6,14 @@ import type { Source } from "../../types.js"; import { useProjectData } from "../../context/project-data.js"; import { CellActions } from "./CellActions.js"; import { ORGANIC_CELL_SHADOW_CLASS } from "./organic-cell-styles.js"; +import { useDashboardI18n } from "../../i18n/index.js"; +import { overviewMessages } from "../../i18n/messages/overview.js"; const statusMap = { - running: { ring: 'border-status-green/50 shadow-[0_0_28px_rgba(0,171,132,0.35)]', text: 'text-status-green', icon: Activity, label: "Running" }, - failed: { ring: 'border-status-red/60 shadow-[0_0_28px_rgba(227,0,15,0.35)]', text: 'text-status-red', icon: XCircle, label: "Failed" }, - intervention: { ring: 'border-status-amber/50 shadow-[0_0_28px_rgba(245,158,11,0.3)]', text: 'text-status-amber', icon: AlertTriangle, label: "Needs Review" }, - idle: { ring: '', text: 'text-slate-400 dark:text-slate-500', icon: FolderGit2, label: "Idle" }, + running: { ring: 'border-status-green/50 shadow-[0_0_28px_rgba(0,171,132,0.35)]', text: 'text-status-green', icon: Activity, labelKey: "sourceStatusRunning" }, + failed: { ring: 'border-status-red/60 shadow-[0_0_28px_rgba(227,0,15,0.35)]', text: 'text-status-red', icon: XCircle, labelKey: "sourceStatusFailed" }, + intervention: { ring: 'border-status-amber/50 shadow-[0_0_28px_rgba(245,158,11,0.3)]', text: 'text-status-amber', icon: AlertTriangle, labelKey: "sourceStatusIntervention" }, + idle: { ring: '', text: 'text-slate-400 dark:text-slate-500', icon: FolderGit2, labelKey: "sourceStatusIdle" }, } as const; interface SourceCellProps { @@ -23,9 +25,13 @@ interface SourceCellProps { export const SourceCell: FunctionComponent = ({ source, isEven, animDelay = 0 }) => { const cellRef = useRef(null); const { selectProject } = useProjectData(); + const { formatNumber, translate, translatePlural } = useDashboardI18n(); const anim = isEven ? 'animate-organic' : 'animate-organic-reverse'; const state = statusMap[source.status] ?? statusMap.idle; const StatusIcon = state.icon; + const statusLabel = translate(overviewMessages, state.labelKey); + const openCount = translatePlural(overviewMessages, "sourceOpenCount", source.openTasks, { formattedCount: formatNumber(source.openTasks) }); + const doneCount = translatePlural(overviewMessages, "sourceDoneCount", source.completedTasks, { formattedCount: formatNumber(source.completedTasks) }); const handleHoverEnter = useCallback(() => { if (!cellRef.current) return; @@ -57,6 +63,7 @@ export const SourceCell: FunctionComponent = ({ source, isEven, onFocus={handleHoverEnter} onBlur={handleHoverLeave} role="group" + aria-label={translate(overviewMessages, "sourceGroupLabel", { name: source.name, status: statusLabel, openCount, doneCount })} tabIndex={0} className="relative group cursor-pointer aspect-square w-[min(14rem,72vw)] max-w-full flex items-center justify-center shrink-0 perspective-1000 focus-visible:ring-2 focus-visible:ring-signal-500/50 focus-visible:rounded-[2rem] focus:outline-none" style={{ animationDelay: `${animDelay}s` }} @@ -82,13 +89,13 @@ export const SourceCell: FunctionComponent = ({ source, isEven,
{/* Status label on hover */}
- - {state.label} +
{/* Main icon */}
- +

@@ -96,14 +103,17 @@ export const SourceCell: FunctionComponent = ({ source, isEven,

- {source.openTasks} open + {openCount} · - {source.completedTasks} done + {doneCount}
{/* Actions */} selectProject(source.id)} onSettingsClick={() => selectProject(source.id)} diff --git a/dashboard/src/v2/components/ui/SprintStreamRow.tsx b/dashboard/src/v2/components/ui/SprintStreamRow.tsx index 396136e5be..846769523c 100644 --- a/dashboard/src/v2/components/ui/SprintStreamRow.tsx +++ b/dashboard/src/v2/components/ui/SprintStreamRow.tsx @@ -2,8 +2,11 @@ import type { FunctionComponent } from "preact"; import { Layers } from "lucide-preact"; import type { Sprint } from "../../types.js"; import { SprintControls } from "../sprints/SprintControls.js"; +import type { SprintControlsLabels } from "../sprints/SprintControls.js"; import type { SprintStreamState } from "../../hooks/use-overview-stream-actions.js"; -import { clampSprintCompletion, formatSprintCompletion } from "../../lib/sprint-progress-display.js"; +import { clampSprintCompletion } from "../../lib/sprint-progress-display.js"; +import { useDashboardI18n } from "../../i18n/index.js"; +import { overviewMessages } from "../../i18n/messages/overview.js"; interface SprintStreamRowProps { sprint: Sprint; @@ -28,20 +31,38 @@ export const SprintStreamRow: FunctionComponent = ({ onStartStop, onPauseResume, }) => { + const { formatNumber, translate, translatePlural } = useDashboardI18n(); const completion = clampSprintCompletion(sprint.completion ?? 0); - const completionLabel = formatSprintCompletion(completion); + const completionLabel = formatNumber(completion / 100, { style: "percent", maximumFractionDigits: 1 }); const accent = state.isPaused ? "bg-status-amber" : state.isActive ? "bg-status-green" : "bg-slate-400 dark:bg-slate-500"; - const statusLabel = state.isPaused ? "Paused" : state.isActive ? "Running" : "Idle"; + const statusLabel = translate(overviewMessages, state.isPaused ? "sprintStatusPaused" : state.isActive ? "sprintStatusRunning" : "sprintStatusIdle"); + const controlsLabels: SprintControlsLabels = { + pause: translate(overviewMessages, "sprintPause"), + resume: translate(overviewMessages, "sprintResume"), + start: translate(overviewMessages, "sprintStart"), + stop: translate(overviewMessages, "sprintStop"), + pending: (action) => translate(overviewMessages, "sprintActionPending", { action }), + pendingLabel: (action, sprintName) => translate(overviewMessages, "sprintActionPendingLabel", { action, name: sprintName }), + actionLabel: (action, sprintName) => translate(overviewMessages, "sprintActionLabel", { action, name: sprintName }), + waitForAction: translate(overviewMessages, "waitForSprintAction"), + waitForActionTitle: translate(overviewMessages, "waitForSprintActionTitle"), + pauseUnavailable: translate(overviewMessages, "sprintPauseUnavailable"), + mustRunToPause: translate(overviewMessages, "sprintMustRunToPause"), + resumeExecution: translate(overviewMessages, "resumeSprintExecution"), + pauseExecution: translate(overviewMessages, "pauseSprintExecution"), + stopExecution: translate(overviewMessages, "stopSprintExecution"), + startExecution: translate(overviewMessages, "startSprintExecution"), + }; return (
@@ -60,7 +81,7 @@ export const SprintStreamRow: FunctionComponent = ({ · - {taskCount} task{taskCount === 1 ? "" : "s"} + {translatePlural(overviewMessages, "taskCount", taskCount, { formattedCount: formatNumber(taskCount) })}

@@ -72,13 +93,13 @@ export const SprintStreamRow: FunctionComponent = ({
- Progress + {translate(overviewMessages, "progress")} {completionLabel}
= ({ onStartStop={onStartStop} onPauseResume={onPauseResume} sprintName={sprint.name} + labels={controlsLabels} />
diff --git a/dashboard/src/v2/components/ui/TaskComposer.tsx b/dashboard/src/v2/components/ui/TaskComposer.tsx index 6636fd92bd..ad2b3eb3cd 100644 --- a/dashboard/src/v2/components/ui/TaskComposer.tsx +++ b/dashboard/src/v2/components/ui/TaskComposer.tsx @@ -13,9 +13,6 @@ import { useReducedMotion } from "../../hooks/use-reduced-motion.js"; import { MODAL_MOTION } from "../../lib/motion/modal-motion.js"; import { AvantgardeSelect } from "./AvantgardeSelect.js"; import { AgentSelectAvatarIcon } from "../agents/AgentSelectAvatarIcon.js"; -import { useOptionalDashboardI18n } from "../../i18n/context.js"; -import { taskMessages } from "../../i18n/messages/tasks.js"; -import { getTaskPriorityLabel, getTaskStatusLabel } from "../../lib/tasks-constants.js"; interface TaskComposerProps { sprints: Sprint[]; @@ -29,6 +26,12 @@ interface TaskComposerProps { const PRIORITY_OPTIONS: TaskPriority[] = ["critical", "high", "medium", "low"]; const STATUS_OPTIONS: TaskStatus[] = ["pending", "in_progress", "completed"]; +const EXECUTOR_OPTIONS: Array<{ value: TaskExecutorType; label: string; description: string }> = [ + { value: "auto", label: "Auto", description: "Use the default Code UX routing." }, + { value: "docker_cli", label: "CLI", description: "Run through the isolated Docker workspace." }, + { value: "jules", label: "Jules", description: "Force remote Jules execution." }, +]; + const pillButtonClass = "transition-[background-color,border-color,color,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-void-900"; export const TaskComposer: FunctionComponent = ({ @@ -43,13 +46,8 @@ export const TaskComposer: FunctionComponent = ({ const cardRef = useRef(null); const titleInputRef = useRef(null); const fieldsRef = useRef(null); - const { locale, translate } = useOptionalDashboardI18n(); - const state = useTaskComposerState(sprints, availableTasks, initialTask, initialSprintId, locale); - const executorOptions: Array<{ value: TaskExecutorType; label: string; description: string }> = [ - { value: "auto", label: "Auto", description: translate(taskMessages, "executorAutoDescription") }, - { value: "docker_cli", label: "CLI", description: translate(taskMessages, "executorCliDescription") }, - { value: "jules", label: "Jules", description: translate(taskMessages, "executorJulesDescription") }, - ]; + + const state = useTaskComposerState(sprints, availableTasks, initialTask, initialSprintId); const reducedMotion = useReducedMotion(); const { feedback, setPending, setSuccess, setError, clearFeedback, clearError } = useActionFeedback(); const sprintTaskCount = availableTasks.filter((task) => task.sprintId === state.sprintId && task.recordId !== initialTask?.recordId).length; @@ -111,18 +109,18 @@ export const TaskComposer: FunctionComponent = ({ state.setIsSubmitting(true); state.setSubmitError(null); clearFeedback(); - setPending(translate(taskMessages, "submittingTask")); + setPending("Submitting task..."); try { await onSubmit(state.getPayload()); state.setIsSubmitting(false); - setSuccess(translate(taskMessages, "submittedTask")); + setSuccess("Task submitted successfully."); onClose(); } catch (err) { state.setIsSubmitting(false); const msg = err instanceof Error ? err.message : String(err); state.setSubmitError(msg); - setError(msg, { retryAction: () => fieldsRef.current?.requestSubmit(), retryLabel: translate(taskMessages, "retry"), autoDismiss: false }); + setError(msg, { retryAction: () => fieldsRef.current?.requestSubmit(), retryLabel: "Retry", autoDismiss: false }); } }; @@ -137,19 +135,19 @@ export const TaskComposer: FunctionComponent = ({
- {translate(taskMessages, state.isEditing ? "editTaskPill" : "taskComposer")} + {state.isEditing ? "Edit Task" : "Task Composer"}

- {translate(taskMessages, state.isEditing ? "refineTask" : "createTask")} + {state.isEditing ? "Refine task" : "Create task"}

- {translate(taskMessages, "composerBody")} + Edit task content, dependencies, execution settings, and the worker agent from one full-height surface.

- +