Files
fusion/packages/dashboard/app/components/RoutingTab.tsx
gsxdsm b760fa0df9 FN-6769: localize remaining dashboard strings
Localize the remaining dashboard UI clusters and refresh the generated i18n resources.

- Replace hardcoded labels across agent, plugin, mission, workflow node, research, document, activity, and miscellaneous dashboard components with app namespace translations.
- Add and synchronize locale catalog entries for English, Spanish, French, Korean, Simplified Chinese, and Traditional Chinese.
- Update i18n lint configuration and generated resource types, with a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-6769-i18n-cluster.md                 |   5 +
 i18next.config.ts                                  |  47 +--
 .../dashboard/app/components/ActiveAgentsPanel.tsx |   2 +-
 packages/dashboard/app/components/ActivityFeed.tsx |  12 +-
 .../dashboard/app/components/ActivityLogModal.tsx  |   4 +-
 packages/dashboard/app/components/AddNodeModal.tsx |   4 +-
 .../dashboard/app/components/AgentDetailView.tsx   |   6 +-
 .../app/components/AgentGenerationModal.tsx        |   2 +-
 .../dashboard/app/components/AgentImportModal.tsx  |   4 +-
 .../app/components/AgentOnboardingModal.tsx        |   4 +-
 .../app/components/AgentPromptsManager.tsx         |   2 +-
 .../dashboard/app/components/AgentRunHistory.tsx   |   2 +-
 .../dashboard/app/components/AgentsOverviewBar.tsx |   2 +-
 .../dashboard/app/components/BranchGroupCard.tsx   |   4 +-
 .../dashboard/app/components/CliBinaryPanel.tsx    |   8 +-
 .../app/components/CursorCliProviderCard.tsx       |   4 +-
 .../dashboard/app/components/DashboardLoader.tsx   |   4 +-
 .../dashboard/app/components/DevServerView.tsx     |   8 +-
 .../dashboard/app/components/DocumentsView.tsx     |   6 +-
 .../dashboard/app/components/ErrorBoundary.tsx     |   9 +-
 .../dashboard/app/components/ExecutorStatusBar.tsx |   5 +-
 .../dashboard/app/components/GitHubStarPrompt.tsx  |  14 +-
 .../dashboard/app/components/GitManagerModal.tsx   |   4 +-
 .../dashboard/app/components/GroupTaskModal.tsx    |   2 +-
 packages/dashboard/app/components/Header.tsx       |   4 +-
 .../dashboard/app/components/HermesRuntimeCard.tsx |   2 +-
 packages/dashboard/app/components/InsightsView.tsx |   2 +-
 .../dashboard/app/components/IssueMentionPopup.tsx |   5 +-
 packages/dashboard/app/components/MailboxView.tsx  |   6 +-
 .../components/MilestoneSliceInterviewModal.tsx    |   2 +-
 .../app/components/MissionInterviewModal.tsx       |   2 +-
 .../dashboard/app/components/MissionManager.tsx    |   8 +-
 .../app/components/ModelOnboardingModal.tsx        |   2 +-
 .../dashboard/app/components/NodeDetailModal.tsx   |   2 +-
 .../app/components/PiExtensionsManager.tsx         |  10 +-
 .../dashboard/app/components/ProjectSelector.tsx   |   6 +-
 packages/dashboard/app/components/QuickChatFAB.tsx |  44 +--
 packages/dashboard/app/components/ResearchView.tsx |   2 +-
 packages/dashboard/app/components/RoutineCard.tsx  |   6 +-
 .../dashboard/app/components/RoutineEditor.tsx     |   2 +-
 packages/dashboard/app/components/RoutingTab.tsx   |   2 +-
 packages/dashboard/app/components/ScheduleCard.tsx |   6 +-
 packages/dashboard/app/components/ScheduleForm.tsx |   2 +-
 packages/dashboard/app/components/ScriptsModal.tsx |   2 +-
 .../app/components/StashConflictModal.tsx          |   4 +-
 .../dashboard/app/components/TerminalModal.tsx     |   2 +-
 .../app/components/nodes/WorkflowNodeTypes.tsx     |  47 +--
 packages/i18n/locales/en/app.json                  | 274 +++++++--------
 packages/i18n/locales/es/app.json                  | 144 ++++++--
 packages/i18n/locales/fr/app.json                  | 144 ++++++--
 packages/i18n/locales/ko/app.json                  | 144 ++++++--
 packages/i18n/locales/zh-CN/app.json               | 144 ++++++--
 packages/i18n/locales/zh-TW/app.json               | 144 ++++++--
 packages/i18n/src/resources.d.ts                   | 375 +++++++++++++++++++--
 54 files changed, 1261 insertions(+), 442 deletions(-)

Fusion-Task-Id: FN-6769

Fusion-Task-Lineage: a8733dc9-3b48-47e6-88c4-020f83ab41de
2026-06-20 05:04:25 -07:00

209 lines
8.4 KiB
TypeScript

import "./RoutingTab.css";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import type { Settings, Task, TaskDetail } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import { fetchNodes, updateTask } from "../api";
import type { NodeInfo } from "../api";
import type { ToastType } from "../hooks/useToast";
import { NodeHealthDot } from "./NodeHealthDot";
interface RoutingTabProps {
task: Task | TaskDetail;
settings?: Settings;
addToast: (message: string, type?: ToastType) => void;
onTaskUpdated?: (task: Task) => void;
}
type RoutingSettings = Settings & {
defaultNodeId?: string;
unavailableNodePolicy?: "block" | "fallback-local";
};
function getRoutingPolicyLabel(policy: RoutingSettings["unavailableNodePolicy"] | undefined, t: (key: string, defaultValue: string) => string): string {
if (policy === "block") return t("routing.policyLabel.block", "Block execution");
if (policy === "fallback-local") return t("routing.policyLabel.fallback", "Fall back to local");
return t("routing.policyLabel.notConfigured", "Not configured");
}
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
function isUnhealthy(status: NodeInfo["status"] | undefined): boolean {
return status !== undefined && status !== "online";
}
export function RoutingTab({ task, settings, addToast, onTaskUpdated }: RoutingTabProps) {
const { t } = useTranslation("app");
const [nodes, setNodes] = useState<NodeInfo[]>([]);
const [loadingNodes, setLoadingNodes] = useState(false);
const [nodesError, setNodesError] = useState<string | null>(null);
const [selectedNodeId, setSelectedNodeId] = useState<string>(task.nodeId ?? "");
const [savingNode, setSavingNode] = useState(false);
const activeTaskIdRef = useRef(task.id);
useEffect(() => {
activeTaskIdRef.current = task.id;
setSelectedNodeId(task.nodeId ?? "");
setSavingNode(false);
}, [task.id, task.nodeId]);
useEffect(() => {
setLoadingNodes(true);
setNodesError(null);
fetchNodes()
.then((result) => {
setNodes(result);
})
.catch((err) => {
setNodesError(getErrorMessage(err) || t("routing.errorLoadingNodes", "Failed to load nodes"));
})
.finally(() => {
setLoadingNodes(false);
});
}, []);
const nodesById = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]);
const sortedNodes = useMemo(
() => [...nodes].sort((a, b) => a.name.localeCompare(b.name)),
[nodes],
);
const routingSettings = settings as RoutingSettings | undefined;
const effectiveNodeId = task.nodeId ?? routingSettings?.defaultNodeId ?? null;
const routingSource = task.nodeId
? t("routing.source.override", "Per-task override")
: routingSettings?.defaultNodeId
? t("routing.source.projectDefault", "Project default")
: t("routing.source.noRouting", "No routing");
const effectiveNode = effectiveNodeId ? nodesById.get(effectiveNodeId) : undefined;
const effectiveNodeName = effectiveNode
? `${effectiveNode.name} (${effectiveNode.type})`
: effectiveNodeId
? `${effectiveNodeId} (${t("routing.nodeUnavailable", "node unavailable or unknown")})`
: t("routing.localNoConfiguration", "Local (no routing configured)");
const isTaskActive = task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string);
const selectorDisabled = isTaskActive || savingNode || loadingNodes;
const handleNodeSelect = useCallback(
async (nextValue: string) => {
if (nextValue === selectedNodeId) {
return;
}
const requestTaskId = task.id;
const previousValue = selectedNodeId;
setSelectedNodeId(nextValue);
setSavingNode(true);
try {
const updatedTask = await updateTask(requestTaskId, { nodeId: nextValue || null });
if (activeTaskIdRef.current !== requestTaskId) return;
setSelectedNodeId(updatedTask.nodeId ?? "");
onTaskUpdated?.(updatedTask);
addToast(nextValue ? t("routing.overrideUpdated", "Node override updated") : t("routing.overrideCleared", "Node override cleared"), "success");
} catch (err) {
if (activeTaskIdRef.current !== requestTaskId) return;
setSelectedNodeId(previousValue);
addToast(getErrorMessage(err) || t("routing.errorUpdatingOverride", "Failed to update node override"), "error");
} finally {
if (activeTaskIdRef.current === requestTaskId) {
setSavingNode(false);
}
}
},
[addToast, onTaskUpdated, selectedNodeId, task.id],
);
const clearOverride = useCallback(() => {
void handleNodeSelect("");
}, [handleNodeSelect]);
return (
<div className="routing-tab">
<h4>{t("routing.title", "Task Routing")}</h4>
<p className="routing-tab__intro">{t("routing.intro", "View the effective execution node and control per-task node override.")}</p>
<section className="routing-tab__section">
<h5>{t("routing.summarySection", "Routing Summary")}</h5>
<div className="routing-summary-grid" role="list">
<div className="routing-summary-row" role="listitem">
<span className="routing-summary-label">{t("routing.effectiveNode", "Effective node")}</span>
<span className="routing-summary-value">
{effectiveNode ? <NodeHealthDot status={effectiveNode.status} compact /> : null}
{effectiveNodeName}
{isUnhealthy(effectiveNode?.status) ? (
<span className="routing-summary-warning">{t("routing.unhealthy", "Unhealthy")}</span>
) : null}
</span>
</div>
<div className="routing-summary-row" role="listitem">
<span className="routing-summary-label">{t("routing.source", "Routing source")}</span>
<span className="routing-summary-value">{routingSource}</span>
</div>
<div className="routing-summary-row" role="listitem">
<span className="routing-summary-label">{t("routing.unavailablePolicy", "Unavailable-node policy")}</span>
<span className="routing-summary-value">{getRoutingPolicyLabel(routingSettings?.unavailableNodePolicy, t)}</span>
</div>
</div>
{isTaskActive && effectiveNodeId ? (
<div className="routing-tab__info-banner">
{t("routing.lockedWhileActive", "Routing is locked while this task is active. Node override cannot be changed until the task is no longer active.")}
</div>
) : null}
</section>
<section className="routing-tab__section">
<h5>{t("routing.overrideSection", "Node Override")}</h5>
{isTaskActive ? (
<div className="routing-tab__warning-banner">
{t("routing.cannotChangeWhileActive", "Node override cannot be changed while the task is active.")}
</div>
) : null}
<label className="routing-tab__selector-label" htmlFor={`routing-node-${task.id}`}>
{t("routing.selectLabel", "Select execution node")}
</label>
<select
id={`routing-node-${task.id}`}
className="select routing-tab__selector"
value={selectedNodeId}
disabled={selectorDisabled}
onChange={(event) => {
void handleNodeSelect(event.target.value);
}}
>
<option value="">{t("routing.useProjectDefault", "Use project default")}</option>
{sortedNodes.map((node) => (
<option key={node.id} value={node.id} title={`Status: ${node.status}`}>
{t("routing.nodeOptionLabel", "{{name}} ({{type}}) — {{status}}", { name: node.name, type: node.type, status: node.status })}
</option>
))}
</select>
{nodesError ? <div className="routing-tab__error">{nodesError}</div> : null}
{task.nodeId ? (
<div className="routing-tab__override-row">
<span className="routing-tab__override-text">
{t("routing.overrideSetTo", "Override set to")}: {nodesById.get(task.nodeId)?.name ?? task.nodeId}
</span>
<button
type="button"
className="btn btn-sm"
disabled={isTaskActive || savingNode}
onClick={clearOverride}
>
{t("routing.clearOverride", "Clear override")}
</button>
</div>
) : null}
</section>
</div>
);
}