FN-6770: localize dashboard workflow and task UI strings

Localize dashboard workflow, task, setup, and pull request UI copy through the shared i18n resources.

- Replace hardcoded dashboard strings in workflow, task detail/chat/comment, setup wizard, PR, and related modal components with translation lookups.
- Add app/common namespace keys across English and secondary locale catalogs, plus regenerated i18n resource types.
- Add the published package changeset and clear the now-localized component files from the i18n lint deferral list.

Files changed:
 .changeset/fn-6770-localize-workflow-task-pr.md    |   5 +
 i18next.config.ts                                  |  17 +-
 packages/dashboard/app/components/Board.tsx        |  10 +-
 .../dashboard/app/components/PrCreateModal.tsx     |  38 +--
 packages/dashboard/app/components/PrPanel.tsx      |   2 +-
 .../dashboard/app/components/PullRequestView.tsx   |  51 ++--
 .../dashboard/app/components/SettingsModal.tsx     |  92 +++----
 .../dashboard/app/components/SetupWizardModal.tsx  |   8 +-
 packages/dashboard/app/components/TaskChatTab.tsx  | 107 ++++----
 packages/dashboard/app/components/TaskComments.tsx |   2 +-
 .../dashboard/app/components/TaskDetailModal.tsx   |  54 ++--
 .../app/components/WorkflowNodeEditor.tsx          | 109 +++++----
 .../app/components/WorkflowResultsTab.tsx          | 190 +++++++-------
 .../dashboard/app/components/WorkflowSelector.tsx  |  20 +-
 packages/i18n/locales/en/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/en/common.json               |  28 +--
 packages/i18n/locales/es/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/es/common.json               |  28 +--
 packages/i18n/locales/fr/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/fr/common.json               |  28 +--
 packages/i18n/locales/ko/app.json                  | 268 ++++++++++++++++++--
 packages/i18n/locales/ko/common.json               |  28 +--
 packages/i18n/locales/zh-CN/app.json               | 268 ++++++++++++++++++--
 packages/i18n/locales/zh-CN/common.json            |  28 +--
 packages/i18n/locales/zh-TW/app.json               | 268 ++++++++++++++++++--
 packages/i18n/locales/zh-TW/common.json            |  28 +--
 packages/i18n/src/resources.d.ts                   | 272 ++++++++++++++++++---
 27 files changed, 2144 insertions(+), 609 deletions(-)

Fusion-Task-Id: FN-6770

Fusion-Task-Lineage: 393b6b8a-1902-4e46-a53e-b4a0793022ca
This commit is contained in:
gsxdsm
2026-06-20 10:35:40 -07:00
parent c0d78f19c5
commit bdf95f8d65
27 changed files with 2144 additions and 609 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Localized the dashboard workflow/task/setup/PR component cluster and removed the obsolete i18n lint deferrals for those files so the hardcoded-string guardrail scans them again.

View File

@@ -6,23 +6,8 @@ import {
const DEFERRED_I18N_LINT_FILES = [
// FNXC:i18n-LintBaseline 2026-06-19-00:00:
// These exact files still carry pre-existing user-facing copy debt after FN-6749 restored the guardrail scope and token suppression.
// Keep the deferral file-scoped and remove entries as those follow-ups localize each cluster.
// FNXC:i18n-LintBaseline 2026-06-20-00:00:
// FN-6771 localized the settings/sections cluster, so those files are no longer deferred and must stay covered by i18n lint.
"packages/dashboard/app/components/WorkflowSelector.tsx",
"packages/dashboard/app/components/WorkflowResultsTab.tsx",
"packages/dashboard/app/components/WorkflowNodeEditor.tsx",
"packages/dashboard/app/components/TaskDetailModal.tsx",
"packages/dashboard/app/components/TaskComments.tsx",
"packages/dashboard/app/components/TaskChatTab.tsx",
"packages/dashboard/app/components/SetupWizardModal.tsx",
"packages/dashboard/app/components/SettingsModal.tsx",
"packages/dashboard/app/components/PullRequestView.tsx",
"packages/dashboard/app/components/PrPanel.tsx",
"packages/dashboard/app/components/PrCreateModal.tsx",
"packages/dashboard/app/components/Board.tsx",
// FN-6770 and FN-6771 localized the remaining workflow/task/setup/PR and settings/sections clusters, so no dashboard component files remain deferred from hardcoded-string lint.
] as const;
/**

View File

@@ -5,6 +5,7 @@ import { Column } from "./Column";
import "./Lane.css";
import type { ToastType } from "../hooks/useToast";
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Pencil, Plus } from "lucide-react";
import { fetchWorkflowSteps, fetchBoardWorkflows, promoteTask, type ModelInfo, type BoardWorkflowDefinition, type BoardWorkflowsPayload } from "../api";
import { useBlockerFanout } from "../hooks/useBlockerFanout";
@@ -124,6 +125,7 @@ function areWorkflowNameLookupsEqual(previous: ReadonlyMap<string, string>, next
}
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow }: BoardProps) {
const { t } = useTranslation("app");
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const archivedLoadedRef = useRef(false);
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
@@ -527,8 +529,8 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
type="button"
className="btn btn-icon btn-sm board-workflow-edit-btn"
onClick={() => onOpenWorkflowEditor(selectedWorkflow.id)}
title="Edit workflows"
aria-label="Edit workflows"
title={t("board.workflow.edit", "Edit workflows")}
aria-label={t("board.workflow.edit", "Edit workflows")}
>
<Pencil size={15} />
</button>
@@ -538,8 +540,8 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
type="button"
className="btn btn-icon btn-sm board-workflow-create-btn"
onClick={onCreateWorkflow}
title="New workflow"
aria-label="New workflow"
title={t("board.workflow.new", "New workflow")}
aria-label={t("board.workflow.new", "New workflow")}
>
<Plus size={15} />
</button>

View File

@@ -488,7 +488,7 @@ export function PrCreateModal({
<>
<section className="pr-create-modal__section">
<h3 className="pr-create-modal__section-title">{t("pr.preflightChecks", "Pre-flight checks")}</h3>
{preflightLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Loading pre-flight checks…</div> : null}
{preflightLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.loadingPreflight", "Loading pre-flight checks…")}</div> : null}
{preflightError ? <div className="form-error pr-error" role="alert"><p>{preflightError}</p></div> : null}
{!preflightLoading && !preflightError ? (
<>
@@ -511,8 +511,8 @@ export function PrCreateModal({
{!preflight?.branchOnRemote ? (
<div className="card pr-create-modal__preflight-remediation">
<div className="pr-create-modal__conflict-copy">
<p className="pr-create-modal__conflict-title">Push branch to remote</p>
<p className="pr-create-modal__conflict-message">Fusion will push this task&apos;s branch to origin so the PR can be created.</p>
<p className="pr-create-modal__conflict-title">{t("pr.pushBranch.title", "Push branch to remote")}</p>
<p className="pr-create-modal__conflict-message">{t("pr.pushBranch.message", "Fusion will push this task's branch to origin so the PR can be created.")}</p>
</div>
<button
type="button"
@@ -521,15 +521,15 @@ export function PrCreateModal({
disabled={pushingBranch || preflightLoading || !baseBranch}
>
{pushingBranch ? <RefreshCw size={14} className="spin" /> : null}
Push branch to remote
{t("pr.pushBranch.button", "Push branch to remote")}
</button>
</div>
) : null}
{preflight?.conflictsWithBase ? (
<div className="card pr-create-modal__conflict-resolution">
<div className="pr-create-modal__conflict-copy">
<p className="pr-create-modal__conflict-title">Resolve conflicts with AI</p>
<p className="pr-create-modal__conflict-message">Fusion will use AI to resolve conflicts on this branch and push it.</p>
<p className="pr-create-modal__conflict-title">{t("pr.resolveConflicts.title", "Resolve conflicts with AI")}</p>
<p className="pr-create-modal__conflict-message">{t("pr.resolveConflicts.message", "Fusion will use AI to resolve conflicts on this branch and push it.")}</p>
</div>
<button
type="button"
@@ -538,7 +538,7 @@ export function PrCreateModal({
disabled={resolvingConflicts || preflightLoading || !baseBranch}
>
{resolvingConflicts ? <RefreshCw size={14} className="spin" /> : null}
Resolve conflicts with AI
{t("pr.resolveConflicts.button", "Resolve conflicts with AI")}
</button>
</div>
) : null}
@@ -554,7 +554,7 @@ export function PrCreateModal({
{userEditedTitle && <button type="button" className="btn btn-sm" onClick={() => { setTitle(aiTitle); setUserEditedTitle(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>}
</div>
</div>
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Generating AI title…</div> : null}
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.generatingTitle", "Generating AI title…")}</div> : null}
{metadataError ? <div className="form-error pr-error" role="alert"><p>{metadataError}</p></div> : null}
<input id="pr-create-modal-title" className="input" value={title} onChange={(event) => { setTitle(event.target.value); setUserEditedTitle(true); }} />
</section>
@@ -567,7 +567,7 @@ export function PrCreateModal({
{userEditedBody && <button type="button" className="btn btn-sm" onClick={() => { setBody(aiBody); setUserEditedBody(false); }}>{t("pr.revertToAi", "Revert to AI version")}</button>}
</div>
</div>
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Generating AI body…</div> : null}
{metadataLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.generatingBody", "Generating AI body…")}</div> : null}
<textarea id="pr-create-modal-body" className="input pr-create-modal__body-input" value={body} onChange={(event) => { setBody(event.target.value); setUserEditedBody(true); }} rows={8} />
{templateUsed && <p className="pr-create-template-hint">{t("pr.usingTemplate", "Using <code>.github/pull_request_template.md</code>")}</p>}
</section>
@@ -575,7 +575,7 @@ export function PrCreateModal({
<section className="pr-create-modal__section pr-create-modal__grid-two">
<div>
<label className="pr-create-modal__label" htmlFor="pr-create-modal-base">{t("pr.baseBranch", "Base branch")}</label>
{optionsLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />Loading PR options…</div> : null}
{optionsLoading ? <div className="pr-create-modal__loading pr-create-modal__section-loading"><span className="status-dot status-dot--pending" aria-hidden="true" />{t("pr.loadingOptions", "Loading PR options…")}</div> : null}
{optionsError ? <div className="form-error pr-error" role="alert"><p>{optionsError}</p></div> : null}
<select id="pr-create-modal-base" className="select" value={baseBranch} onChange={(event) => void handleBaseChange(event.target.value)} disabled={optionsLoading || Boolean(optionsError) || (options?.baseBranches?.length ?? 0) === 0}>
{(options?.baseBranches ?? []).map((branch) => <option key={branch} value={branch}>{branch}</option>)}
@@ -588,7 +588,7 @@ export function PrCreateModal({
</section>
<OptionChips
label="Reviewers"
label={t("pr.reviewers", "Reviewers")}
options={options?.reviewers ?? []}
selected={reviewers}
onChange={setReviewers}
@@ -596,7 +596,7 @@ export function PrCreateModal({
getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`}
/>
<OptionChips
label="Assignees"
label={t("pr.assignees", "Assignees")}
options={options?.assignees ?? []}
selected={assignees}
onChange={setAssignees}
@@ -604,7 +604,7 @@ export function PrCreateModal({
getLabel={(option) => option.name ? `${option.name} (@${option.login})` : `@${option.login}`}
/>
<OptionChips
label="Labels"
label={t("pr.labels", "Labels")}
options={options?.labels ?? []}
selected={labels}
onChange={setLabels}
@@ -617,7 +617,7 @@ export function PrCreateModal({
<summary>{t("pr.previewTitle", "Diff & commit preview")}</summary>
<div className="pr-create-modal__preview">
<h4>{t("pr.commitsLabel", "Commits")}</h4>
{!preflightLoading && !preflightError && (preflight?.commits?.length ?? 0) === 0 ? <p className="pr-create-template-hint">No commits found.</p> : null}
{!preflightLoading && !preflightError && (preflight?.commits?.length ?? 0) === 0 ? <p className="pr-create-template-hint">{t("pr.noCommits", "No commits found.")}</p> : null}
{(preflight?.commits ?? []).map((commit) => (
<div className="pr-create-modal__commit-row" key={commit.sha}>
<code>{commit.sha.slice(0, 7)}</code>
@@ -626,7 +626,7 @@ export function PrCreateModal({
</div>
))}
<h4>{t("pr.changedFilesLabel", "Changed files")}</h4>
{!preflightLoading && !preflightError && (preflight?.changedFiles?.length ?? 0) === 0 ? <p className="pr-create-template-hint">No changed files detected.</p> : null}
{!preflightLoading && !preflightError && (preflight?.changedFiles?.length ?? 0) === 0 ? <p className="pr-create-template-hint">{t("pr.noChangedFiles", "No changed files detected.")}</p> : null}
{(preflight?.changedFiles ?? []).map((file) => (
<div className="pr-create-modal__file-row" key={file.path}>
<span>{file.path}</span>
@@ -641,7 +641,7 @@ export function PrCreateModal({
<div className="form-error pr-error" role="alert">
<p>{pushBranchError}</p>
<div className="pr-error__actions">
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setPushBranchError(null)} aria-label="Dismiss push branch error">×</button>
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setPushBranchError(null)} aria-label={t("pr.dismissPushBranchError", "Dismiss push branch error")}>×</button>
</div>
</div>
) : null}
@@ -650,7 +650,7 @@ export function PrCreateModal({
<div className="form-error pr-error" role="alert">
<p>{resolveConflictError}</p>
<div className="pr-error__actions">
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setResolveConflictError(null)} aria-label="Dismiss conflict resolution error">×</button>
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => setResolveConflictError(null)} aria-label={t("pr.dismissConflictResolutionError", "Dismiss conflict resolution error")}>×</button>
</div>
</div>
) : null}
@@ -660,8 +660,8 @@ export function PrCreateModal({
<p>{submitError}</p>
{lastGhError?.hint ? <p className="pr-error__hint">{lastGhError.hint}</p> : null}
<div className="pr-error__actions">
{lastGhError?.action?.kind === "shell" ? <p>Action: run <code>{lastGhError.action.command}</code></p> : null}
{lastGhError?.action?.kind === "open" ? <p>Action: open <a href={lastGhError.action.url} target="_blank" rel="noreferrer">docs</a></p> : null}
{lastGhError?.action?.kind === "shell" ? <p>{t("pr.error.actionRun", "Action: run")} <code>{lastGhError.action.command}</code></p> : null}
{lastGhError?.action?.kind === "open" ? <p>{t("pr.error.actionOpen", "Action: open")} <a href={lastGhError.action.url} target="_blank" rel="noreferrer">{t("pr.error.docs", "docs")}</a></p> : null}
{lastGhError?.retryable ? <button type="button" className="btn btn-sm pr-error__retry" onClick={() => void submit()}>{t("actions.retry", "Retry")}</button> : null}
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => { setLastGhError(null); setSubmitError(null); }} aria-label={t("pr.dismissError", "Dismiss PR error")}>×</button>
</div>

View File

@@ -238,7 +238,7 @@ function PrCard({
<div>{lastGhError.message}</div>
{lastGhError.hint ? <div className="pr-error__hint">{lastGhError.hint}</div> : null}
<div className="pr-error__actions">
{lastGhError.action?.kind === "shell" ? <div>Action: run <code>{lastGhError.action.command}</code></div> : null}
{lastGhError.action?.kind === "shell" ? <div>{t("pr.error.actionRun", "Action: run")} <code>{lastGhError.action.command}</code></div> : null}
{lastGhError.retryable ? <button className="btn btn-sm pr-error__retry" onClick={() => void handleRefresh()}>{t("git.retryButton", "Retry")}</button> : null}
<button className="btn btn-sm pr-error__dismiss" onClick={() => setLastGhError(null)} aria-label={t("git.dismissPrError", "Dismiss PR error")}>×</button>
</div>

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
GitPullRequest,
GitMerge,
@@ -101,6 +102,7 @@ function ChecksIcon({ rollup }: { rollup: string }) {
}
export function PullRequestView(props: PullRequestViewProps) {
const { t } = useTranslation("app");
const { detail: detailProp, pullRequestId, projectId, onAction, loadPullRequest } = props;
const [detail, setDetail] = useState<PrDetail | null>(detailProp ?? null);
const [error, setError] = useState<string | null>(null);
@@ -166,7 +168,7 @@ export function PullRequestView(props: PullRequestViewProps) {
if (!detail) {
return (
<div className="pr-view pr-view--loading" data-testid="pr-view-loading">
Loading PR…
{t("pr.view.loading", "Loading PR…")}
</div>
);
}
@@ -179,7 +181,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-view" data-testid="pr-view" data-state="creating">
<PrIdentityHeader detail={detail} />
<div className="pr-placeholder" data-testid="pr-creating">
<Clock size={16} /> Creating PR…
<Clock size={16} /> {t("pr.view.creating", "Creating PR…")}
</div>
</div>
);
@@ -192,7 +194,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<PrIdentityHeader detail={detail} />
<div className="pr-error-reason" data-testid="pr-failed">
<AlertTriangle size={16} className="pr-icon-failure" />
<span>{detail.failureReason ?? "PR creation failed"}</span>
<span>{detail.failureReason ?? t("pr.view.creationFailed", "PR creation failed")}</span>
</div>
<div className="pr-action-bar">
<button
@@ -202,7 +204,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "retry-create"}
onClick={() => void runAction("retry-create")}
>
<RotateCcw size={14} /> Retry PR creation
<RotateCcw size={14} /> {t("pr.view.retryCreation", "Retry PR creation")}
</button>
</div>
{error && <div className="pr-inline-error">{error}</div>}
@@ -216,7 +218,7 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-view" data-testid="pr-view" data-state="unverified">
<PrIdentityHeader detail={detail} />
<div className="pr-notice pr-notice--unverified" data-testid="pr-unverified">
<Clock size={16} /> Verifying with GitHub…
<Clock size={16} /> {t("pr.view.verifyingGithub", "Verifying with GitHub…")}
</div>
<div className="pr-action-bar">
<button
@@ -224,9 +226,9 @@ export function PullRequestView(props: PullRequestViewProps) {
className="pr-action"
data-testid="pr-merge"
disabled
title="Merge is disabled until GitHub verifies this PR"
title={t("pr.view.mergeDisabledUntilVerified", "Merge is disabled until GitHub verifies this PR")}
>
<GitMerge size={14} /> Merge
<GitMerge size={14} /> {t("pr.view.merge", "Merge")}
</button>
</div>
{/* checks/threads hidden while unverified */}
@@ -243,8 +245,7 @@ export function PullRequestView(props: PullRequestViewProps) {
{/* responding banner */}
{state === "responding" && (
<div className="pr-banner pr-banner--responding" data-testid="pr-responding">
<MessageSquare size={16} /> Response run in progress — {summary.pendingThreads} threads
pending
<MessageSquare size={16} /> {t("pr.view.responsePending", "Response run in progress — {{count}} threads pending", { count: summary.pendingThreads })}
</div>
)}
@@ -257,17 +258,17 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={state === "responding" || busy === "approve"}
onClick={() => void runAction("approve")}
>
<ThumbsUp size={14} /> Approve
<ThumbsUp size={14} /> {t("pr.view.approve", "Approve")}
</button>
<button
type="button"
className="pr-action pr-action--retry"
data-testid="pr-retry"
disabled={state === "responding" || busy === "retry"}
title={state === "responding" ? "A response run is already in progress" : undefined}
title={state === "responding" ? t("pr.view.responseAlreadyInProgress", "A response run is already in progress") : undefined}
onClick={() => void runAction("retry")}
>
<RotateCcw size={14} /> Request retry
<RotateCcw size={14} /> {t("pr.view.requestRetry", "Request retry")}
</button>
{!confirmingMerge ? (
<button
@@ -275,10 +276,10 @@ export function PullRequestView(props: PullRequestViewProps) {
className="pr-action pr-action--merge"
data-testid="pr-merge"
disabled={conflicting || state === "responding" || busy === "merge"}
title={conflicting ? "Resolve conflicts on GitHub before merging" : undefined}
title={conflicting ? t("pr.view.resolveConflictsBeforeMerge", "Resolve conflicts on GitHub before merging") : undefined}
onClick={() => setConfirmingMerge(true)}
>
<GitMerge size={14} /> Merge
<GitMerge size={14} /> {t("pr.view.merge", "Merge")}
</button>
) : (
<button
@@ -288,7 +289,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "merge"}
onClick={() => void runAction("merge")}
>
<GitMerge size={14} /> Confirm merge
<GitMerge size={14} /> {t("pr.view.confirmMerge", "Confirm merge")}
</button>
)}
<button
@@ -298,7 +299,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "close"}
onClick={() => void runAction("close")}
>
<XCircle size={14} /> Close
<XCircle size={14} /> {t("pr.view.close", "Close")}
</button>
<label className="pr-automerge-toggle" data-testid="pr-automerge">
@@ -308,7 +309,7 @@ export function PullRequestView(props: PullRequestViewProps) {
disabled={busy === "automerge"}
onChange={(e) => void runAction("automerge", { enabled: e.target.checked })}
/>
<span>Auto-merge</span>
<span>{t("pr.view.autoMerge", "Auto-merge")}</span>
<span className="pr-automerge-gate" data-testid="pr-automerge-gate">
{summary.autoMergeReason}
</span>
@@ -324,17 +325,17 @@ export function PullRequestView(props: PullRequestViewProps) {
target="_blank"
rel="noopener noreferrer"
>
Resolve conflicts on GitHub <ExternalLink size={12} />
{t("pr.view.resolveConflictsOnGithub", "Resolve conflicts on GitHub")} <ExternalLink size={12} />
</a>
)}
{/* ── merge-readiness summary ─────────────────────────────────────── */}
<div className="pr-summary" data-testid="pr-summary">
<span className="pr-summary-item" data-testid="pr-summary-mergeable">
Mergeable: {summary.mergeable}
{t("pr.view.mergeableLabel", "Mergeable:")} {summary.mergeable}
</span>
<span className="pr-summary-item" data-testid="pr-summary-review">
Review: {summary.reviewDecision ?? "none"}
{t("pr.view.reviewLabel", "Review:")} {summary.reviewDecision ?? t("pr.view.none", "none")}
</span>
<span className="pr-summary-item" data-testid="pr-summary-checks">
<ChecksIcon rollup={summary.checksRollup} /> {summary.checksRollup}
@@ -344,7 +345,7 @@ export function PullRequestView(props: PullRequestViewProps) {
{/* ── threads (agent replies nested) ───────────────────────────────── */}
<div className="pr-threads" data-testid="pr-threads">
{detail.threads.length === 0 ? (
<div className="pr-threads-empty">No review threads.</div>
<div className="pr-threads-empty">{t("pr.view.noReviewThreads", "No review threads.")}</div>
) : (
detail.threads.map((thread) => (
<div
@@ -358,24 +359,24 @@ export function PullRequestView(props: PullRequestViewProps) {
<div className="pr-thread-head">
{thread.outcome === "pending" && (
<span className="pr-thread-pending">
<Clock size={12} /> pending
<Clock size={12} /> {t("pr.view.threadPending", "pending")}
</span>
)}
{thread.outcome === "disagreed" && (
<span className="pr-thread-disagreed">
<AlertTriangle size={12} /> agent disagreed
<AlertTriangle size={12} /> {t("pr.view.agentDisagreed", "agent disagreed")}
</span>
)}
{thread.outcome === "fixed" && (
<span className="pr-thread-fixed">
<CheckCircle size={12} /> fixed
<CheckCircle size={12} /> {t("pr.view.threadFixed", "fixed")}
</span>
)}
<span className="pr-thread-id">{thread.threadId}</span>
</div>
{thread.fixCommitSha && (
<div className="pr-thread-reply" data-testid="pr-thread-reply">
Agent reply — fix {thread.fixCommitSha.slice(0, 8)}
{t("pr.view.agentReplyFix", "Agent reply — fix {{sha}}", { sha: thread.fixCommitSha.slice(0, 8) })}
</div>
)}
</div>

View File

@@ -1850,10 +1850,10 @@ export function SettingsModal({
const info = await fetchBackups(projectId);
setBackupInfo(info);
} else {
addToast(result.error || "Failed to create backup", "error");
addToast(result.error || t("settings.backups.createFailed", "Failed to create backup"), "error");
}
} catch (err) {
addToast(getErrorMessage(err) || "Failed to create backup", "error");
addToast(getErrorMessage(err) || t("settings.backups.createFailed", "Failed to create backup"), "error");
} finally {
setBackupLoading(false);
}
@@ -1879,10 +1879,14 @@ export function SettingsModal({
document.body.removeChild(link);
URL.revokeObjectURL(url);
const scopeLabel = scope === "global" ? "global" : scope === "project" ? "project" : "all";
addToast(`Settings exported (${scopeLabel} scope)`, "success");
const scopeLabel = scope === "global"
? t("settings.importExport.scopeLabel.global", "global")
: scope === "project"
? t("settings.importExport.scopeLabel.project", "project")
: t("settings.importExport.scopeLabel.all", "all");
addToast(t("settings.importExport.exported", "Settings exported ({{scope}} scope)", { scope: scopeLabel }), "success");
} catch (err) {
addToast(getErrorMessage(err) || "Failed to export settings", "error");
addToast(getErrorMessage(err) || t("settings.importExport.exportFailed", "Failed to export settings"), "error");
}
}, [addToast, activeSectionScope, projectId]);
@@ -1899,7 +1903,7 @@ export function SettingsModal({
setImportPreview(data);
setImportDialogOpen(true);
} catch (err) {
addToast(`Invalid JSON file: ${getErrorMessage(err)}`, "error");
addToast(t("settings.importExport.invalidJson", "Invalid JSON file: {{error}}", { error: getErrorMessage(err) }), "error");
setImportFile(null);
} finally {
setImportLoading(false);
@@ -1914,10 +1918,10 @@ export function SettingsModal({
const result = await importSettings(importPreview, { scope: importScope, merge: importMerge }, projectId);
if (result.success) {
const parts: string[] = [];
if (result.globalCount > 0) parts.push(`${result.globalCount} global`);
if (result.projectCount > 0) parts.push(`${result.projectCount} project`);
if (result.workflowSettingsCount > 0) parts.push(`${result.workflowSettingsCount} workflow setting value(s)`);
addToast(`Imported ${parts.join(", ")} setting(s)`, "success");
if (result.globalCount > 0) parts.push(t("settings.importExport.counts.global", "{{count}} global", { count: result.globalCount }));
if (result.projectCount > 0) parts.push(t("settings.importExport.counts.project", "{{count}} project", { count: result.projectCount }));
if (result.workflowSettingsCount > 0) parts.push(t("settings.importExport.counts.workflowSettings", "{{count}} workflow setting value", { count: result.workflowSettingsCount }));
addToast(t("settings.importExport.imported", "Imported {{counts}} setting(s)", { counts: parts.join(", ") }), "success");
setImportDialogOpen(false);
setImportPreview(null);
setImportFile(null);
@@ -1925,10 +1929,10 @@ export function SettingsModal({
const refreshed = await fetchSettings(projectId);
setForm(refreshed);
} else {
addToast(result.error || "Import failed", "error");
addToast(result.error || t("settings.importExport.importFailed", "Import failed"), "error");
}
} catch (err) {
addToast(getErrorMessage(err) || "Failed to import settings", "error");
addToast(getErrorMessage(err) || t("settings.importExport.importFailedDetailed", "Failed to import settings"), "error");
} finally {
setImportLoading(false);
}
@@ -2373,11 +2377,11 @@ export function SettingsModal({
const result = await installQmd(projectId);
await refreshMemoryBackend();
addToast(
result.qmdAvailable ? "qmd installed successfully" : "qmd install finished, but qmd is still unavailable",
result.qmdAvailable ? t("settings.memory.qmdInstalled", "qmd installed successfully") : t("settings.memory.qmdInstallUnavailable", "qmd install finished, but qmd is still unavailable"),
result.qmdAvailable ? "success" : "warning",
);
} catch (err) {
addToast(getErrorMessage(err) || "Failed to install qmd", "error");
addToast(getErrorMessage(err) || t("settings.memory.qmdInstallFailed", "Failed to install qmd"), "error");
} finally {
setQmdInstallLoading(false);
}
@@ -2471,14 +2475,14 @@ export function SettingsModal({
try {
const result = await installCloudflared(projectId);
if (!result.success) {
setCloudflaredInstallError(result.error ?? "Installation failed");
setCloudflaredInstallError(result.error ?? t("settings.remote.installationFailed", "Installation failed"));
return;
}
const status = await fetchRemoteStatus(projectId);
setRemoteStatus(status);
addToast(t("settings.remote.cloudflaredInstalled", "cloudflared installed successfully"), "success");
} catch (err) {
setCloudflaredInstallError(err instanceof Error ? err.message : "Installation failed");
setCloudflaredInstallError(err instanceof Error ? err.message : t("settings.remote.installationFailed", "Installation failed"));
} finally {
setCloudflaredInstalling(false);
}
@@ -2871,15 +2875,15 @@ export function SettingsModal({
target="_blank"
rel="noopener noreferrer"
className="settings-github-star-btn"
aria-label="Star Fusion on GitHub"
title="Star Fusion on GitHub"
aria-label={t("settings.header.starFusion", "Star Fusion on GitHub")}
title={t("settings.header.starFusion", "Star Fusion on GitHub")}
onClick={markStarClicked}
data-clicked={starClicked ? "true" : "false"}
>
<span className="settings-github-star-btn__action">
<ProviderIcon provider="github" size="sm" />
<Star size={11} aria-hidden="true" />
Star
{t("settings.header.star", "Star")}
</span>
{gitHubStarCount !== null && (
<span className="settings-github-star-btn__count" aria-label={`${gitHubStarCount.toLocaleString()} stars`}>
@@ -2894,14 +2898,14 @@ export function SettingsModal({
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm settings-header-discord-btn"
aria-label="Join our Discord"
title="Join our Discord"
aria-label={t("settings.header.joinDiscord", "Join our Discord")}
title={t("settings.header.joinDiscord", "Join our Discord")}
>
<DiscordIcon size={13} />
{t("settings.header.discord", "Discord")}
</a>
</div>
<button className="modal-close" onClick={onClose} aria-label="Close">
<button className="modal-close" onClick={onClose} aria-label={t("actions.close", "Close")}>
&times;
</button>
</div>
@@ -2971,8 +2975,8 @@ export function SettingsModal({
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm settings-footer-help-btn"
aria-label="Help and discussions"
title="Help and discussions"
aria-label={t("settings.footer.helpDiscussions", "Help and discussions")}
title={t("settings.footer.helpDiscussions", "Help and discussions")}
>
<HelpCircle size={13} aria-hidden="true" />
{t("settings.footer.help", "Help")}
@@ -2986,8 +2990,8 @@ export function SettingsModal({
void handleCheckForUpdates();
}}
disabled={updateCheckLoading}
aria-label="Check for updates"
title="Check for updates"
aria-label={t("settings.footer.checkUpdates", "Check for updates")}
title={t("settings.footer.checkUpdates", "Check for updates")}
>
<span className="settings-modal-version">{t("settings.footer.version", "Version {{version}}", { version: appVersion })}</span>
<RefreshCw size={12} className={updateCheckLoading ? "spinning" : undefined} />
@@ -3030,7 +3034,7 @@ export function SettingsModal({
className="btn btn-sm"
onClick={() => fileInputRef.current?.click()}
disabled={importLoading}
title="Import settings from JSON file"
title={t("settings.importExport.importTitleAttr", "Import settings from JSON file")}
>
{importLoading ? t("settings.importExport.loadingFile", "Loading…") : t("settings.importExport.importBtn", "Import")}
</button>
@@ -3052,18 +3056,18 @@ export function SettingsModal({
onClick={handleOverlapPathPickerOverlayClick}
role="dialog"
aria-modal="true"
aria-label="Browse workspace path"
aria-label={t("settings.scheduling.browseWorkspacePath", "Browse workspace path")}
>
<div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h3>{t("settings.scheduling.selectIgnoredOverlapPath", "Select ignored overlap path")}</h3>
<button className="modal-close" onClick={closeOverlapPathPicker} aria-label="Close">
<button className="modal-close" onClick={closeOverlapPathPicker} aria-label={t("actions.close", "Close")}>
&times;
</button>
</div>
<div className="modal-body settings-overlap-path-picker-body">
<p className="settings-overlap-path-picker-note">
Choose a file to ignore directly, or navigate into a folder and select the current directory.
{t("settings.scheduling.overlapPickerNote", "Choose a file to ignore directly, or navigate into a folder and select the current directory.")}
</p>
<FileBrowser
entries={overlapPathPickerEntries}
@@ -3080,7 +3084,7 @@ export function SettingsModal({
<div className="modal-actions">
<div className="modal-actions-left">
<small>
Current directory: <code>{overlapPathPickerCurrentPath === "." ? "(project root)" : overlapPathPickerCurrentPath}</code>
{t("settings.fileBrowser.currentDirectory", "Current directory:")} <code>{overlapPathPickerCurrentPath === "." ? t("settings.fileBrowser.projectRoot", "(project root)") : overlapPathPickerCurrentPath}</code>
</small>
</div>
<div className="modal-actions-right">
@@ -3106,18 +3110,18 @@ export function SettingsModal({
onClick={handleWorktreesDirPickerOverlayClick}
role="dialog"
aria-modal="true"
aria-label="Browse worktrees directory"
aria-label={t("settings.worktrees.browseWorktreesDirectory", "Browse worktrees directory")}
>
<div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h3>{t("settings.worktrees.selectWorktreesDir", "Select worktrees directory")}</h3>
<button className="modal-close" onClick={closeWorktreesDirPicker} aria-label="Close">
<button className="modal-close" onClick={closeWorktreesDirPicker} aria-label={t("actions.close", "Close")}>
&times;
</button>
</div>
<div className="modal-body settings-overlap-path-picker-body">
<p className="settings-overlap-path-picker-note">
Navigate to the folder where Fusion should create task worktrees, then select the current directory.
{t("settings.worktrees.worktreesPickerNote", "Navigate to the folder where Fusion should create task worktrees, then select the current directory.")}
</p>
<FileBrowser
entries={worktreesDirPickerEntries}
@@ -3134,7 +3138,7 @@ export function SettingsModal({
<div className="modal-actions">
<div className="modal-actions-left">
<small>
Current directory: <code>{worktreesDirPickerCurrentPath === "." ? "(project root)" : worktreesDirPickerCurrentPath}</code>
{t("settings.fileBrowser.currentDirectory", "Current directory:")} <code>{worktreesDirPickerCurrentPath === "." ? t("settings.fileBrowser.projectRoot", "(project root)") : worktreesDirPickerCurrentPath}</code>
</small>
</div>
<div className="modal-actions-right">
@@ -3156,7 +3160,7 @@ export function SettingsModal({
<div className="modal modal-md">
<div className="modal-header">
<h3>{t("settings.importExport.importTitle", "Import Settings")}</h3>
<button className="modal-close" onClick={() => setImportDialogOpen(false)} aria-label="Close">
<button className="modal-close" onClick={() => setImportDialogOpen(false)} aria-label={t("actions.close", "Close")}>
&times;
</button>
</div>
@@ -3165,7 +3169,7 @@ export function SettingsModal({
{importPreview.global && Object.keys(importPreview.global).length > 0 && (
<div className="form-group">
<strong>Global Settings:</strong>
<strong>{t("settings.importExport.globalSettings", "Global Settings:")}</strong>
<ul className="import-preview-list">
{Object.entries(importPreview.global)
.filter(([, v]) => v !== undefined)
@@ -3178,7 +3182,7 @@ export function SettingsModal({
{importPreview.project && Object.keys(importPreview.project).length > 0 && (
<div className="form-group">
<strong>Project Settings:</strong>
<strong>{t("settings.importExport.projectSettings", "Project Settings:")}</strong>
<ul className="import-preview-list">
{Object.entries(importPreview.project)
.filter(([, v]) => v !== undefined)
@@ -3190,15 +3194,15 @@ export function SettingsModal({
)}
<div className="form-group">
<label htmlFor="import-scope">Import Scope:</label>
<label htmlFor="import-scope">{t("settings.importExport.importScope", "Import Scope:")}</label>
<select
id="import-scope"
value={importScope}
onChange={(e) => setImportScope(e.target.value as 'global' | 'project' | 'both')}
>
<option value="both">Both global and project settings</option>
<option value="global">Global settings only</option>
<option value="project">Project settings only</option>
<option value="both">{t("settings.importExport.scopeBoth", "Both global and project settings")}</option>
<option value="global">{t("settings.importExport.scopeGlobal", "Global settings only")}</option>
<option value="project">{t("settings.importExport.scopeProject", "Project settings only")}</option>
</select>
</div>
@@ -3210,9 +3214,9 @@ export function SettingsModal({
checked={importMerge}
onChange={(e) => setImportMerge(e.target.checked)}
/>
Merge with existing settings (recommended)
{t("settings.importExport.mergeExisting", "Merge with existing settings (recommended)")}
</label>
<small>If unchecked, existing settings will be replaced with imported values.</small>
<small>{t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")}</small>
</div>
</div>
<div className="modal-actions">

View File

@@ -152,14 +152,14 @@ export function SetupWizardModal({
{/* Header */}
<div className="setup-wizard-header">
<div className="setup-wizard-heading">
<div className="setup-wizard-brand" aria-label="Fusion">
<div className="setup-wizard-brand" aria-label={t("setup.brandName", "Fusion")}>
<svg
className="setup-wizard-brand-logo"
width={28}
height={28}
viewBox="0 0 128 128"
fill="none"
aria-label="Fusion logo"
aria-label={t("setup.brandLogo", "Fusion logo")}
role="img"
>
<circle
@@ -174,12 +174,12 @@ export function SetupWizardModal({
fill="currentColor"
/>
</svg>
<span className="setup-wizard-brand-name">Fusion</span>
<span className="setup-wizard-brand-name">{t("setup.brandName", "Fusion")}</span>
</div>
<h2 id="wizard-title" className="setup-wizard-title">
{state.step === "auth" && t("setup.setAuthToken", "Set Auth Token")}
{state.step === "manual" && t("setup.welcomeToFusion", "Welcome to Fusion")}
{state.step === "complete" && t("setup.setupComplete", "Setup Complete!")}
{state.step === "complete" && t("setup.setupCompleteTitle", "Setup Complete!")}
</h2>
</div>
{state.step !== "complete" && (

View File

@@ -3,6 +3,8 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ChevronDown, Loader2, Maximize2, Minimize2, Send } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { addSteeringComment, refineTask } from "../api";
import { useAgentLogs } from "../hooks/useAgentLogs";
import type { ToastType } from "../hooks/useToast";
@@ -58,18 +60,18 @@ function isTranscriptNearBottom(container: HTMLElement): boolean {
return container.scrollHeight - (container.scrollTop + container.clientHeight) <= BOTTOM_FOLLOW_THRESHOLD;
}
function getRoleLabel(role: AgentLogRole): string {
function getRoleLabel(role: AgentLogRole, t: TFunction<"app">): string {
switch (role) {
case "triage":
return "Planner";
return t("taskChat.roles.planner", "Planner");
case "executor":
return "Executor";
return t("taskChat.roles.executor", "Executor");
case "reviewer":
return "Reviewer";
return t("taskChat.roles.reviewer", "Reviewer");
case "merger":
return "Merger";
return t("taskChat.roles.merger", "Merger");
default:
return "Agent";
return t("taskChat.roles.agent", "Agent");
}
}
@@ -137,7 +139,7 @@ function mergeUserMessages(persistedComments: readonly SteeringComment[] | undef
return messages;
}
function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: readonly UserChatMessage[]): TaskChatTranscriptItem[] {
function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: readonly UserChatMessage[], t: TFunction<"app">): TaskChatTranscriptItem[] {
const orderedItems = [
...entries.map((entry, index) => ({ kind: "agent" as const, entry, index, timestamp: getTimestampMs(entry.timestamp) })),
...userMessages.map((message, index) => ({ kind: "user" as const, message, index, timestamp: getTimestampMs(message.createdAt) })),
@@ -155,7 +157,7 @@ function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: r
previousItem.entries.push(item.entry);
return items;
}
items.push({ kind: "agent", role, label: getRoleLabel(role), entries: [item.entry] });
items.push({ kind: "agent", role, label: getRoleLabel(role, t), entries: [item.entry] });
return items;
}, []);
}
@@ -178,25 +180,29 @@ function isToolLikeEntry(entry: AgentLogEntry): boolean {
return entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error";
}
function formatEntryLabel(entry: AgentLogEntry): string {
function formatEntryLabel(entry: AgentLogEntry, t: TFunction<"app">): string {
switch (entry.type) {
case "tool":
return "Tool call";
return t("taskChat.toolCall", "Tool call");
case "tool_result":
return "Tool result";
return t("taskChat.toolResult", "Tool result");
case "tool_error":
return "Tool error";
return t("taskChat.toolError", "Tool error");
case "thinking":
return "Thinking";
return t("taskChat.thinking", "Thinking");
default:
return "Message";
return t("taskChat.message", "Message");
}
}
function formatCompletionLabel(entry: AgentLogEntry, t: TFunction<"app">): string {
return entry.type === "tool_error" ? t("taskChat.error", "Error") : t("taskChat.result", "Result");
}
const TOOL_NAME_SUMMARY_LIMIT = 5;
function formatToolCallCount(count: number): string {
return count === 1 ? "1 tool call" : `${count} tool calls`;
function formatToolCallCount(count: number, t: TFunction<"app">): string {
return t("taskChat.toolCallCount", "{{count}} tool call", { count });
}
function getToolInvocationEntries(entries: AgentLogEntry[]): AgentLogEntry[] {
@@ -270,12 +276,14 @@ function TaskChatText({ entries }: { entries: AgentLogEntry[] }) {
}
function TaskChatToolEntry({ entry }: { entry: AgentLogEntry }) {
const { t } = useTranslation("app");
return (
<article
className={`task-chat-tool-entry task-chat-tool-entry--${entry.type.replace("_", "-")}`}
data-testid={`task-chat-entry-${entry.type}`}
>
<div className="task-chat-entry-kicker">{formatEntryLabel(entry)}</div>
<div className="task-chat-entry-kicker">{formatEntryLabel(entry, t)}</div>
<div className="task-chat-entry-text">{entry.text}</div>
{entry.detail ? <pre className="task-chat-tool-detail">{linkifyFilePaths(entry.detail)}</pre> : null}
</article>
@@ -310,25 +318,26 @@ function getToolGroupRows(entries: AgentLogEntry[]): TaskChatToolGroupRow[] {
}
function TaskChatToolInvocation({ row }: { row: Extract<TaskChatToolGroupRow, { kind: "invocation" }> }) {
const { t } = useTranslation("app");
const completion = row.completion;
const completionLabel = completion ? formatEntryLabel(completion).replace("Tool ", "") : undefined;
const completionLabel = completion ? formatCompletionLabel(completion, t) : undefined;
const className = `task-chat-tool-entry task-chat-tool-invocation${completion?.type === "tool_error" ? " task-chat-tool-entry--tool-error" : ""}`;
return (
<article className={className} data-testid="task-chat-tool-invocation">
<div className="task-chat-entry-kicker">
{completionLabel ? `Tool call → ${completionLabel}` : "Tool call"}
{completionLabel ? t("taskChat.toolCallTo", "Tool call → {{label}}", { label: completionLabel }) : t("taskChat.toolCall", "Tool call")}
</div>
<div className="task-chat-entry-text">{row.call.text}</div>
{row.call.detail ? (
<div className="task-chat-tool-detail-block">
<div className="task-chat-tool-detail-label">Arguments</div>
<div className="task-chat-tool-detail-label">{t("taskChat.arguments", "Arguments")}</div>
<pre className="task-chat-tool-detail">{linkifyFilePaths(row.call.detail)}</pre>
</div>
) : null}
{completion?.detail ? (
<div className="task-chat-tool-detail-block">
<div className="task-chat-tool-detail-label">{completion.type === "tool_error" ? "Error" : "Result"}</div>
<div className="task-chat-tool-detail-label">{completion.type === "tool_error" ? t("taskChat.error", "Error") : t("taskChat.result", "Result")}</div>
<pre className="task-chat-tool-detail">{linkifyFilePaths(completion.detail)}</pre>
</div>
) : null}
@@ -337,6 +346,7 @@ function TaskChatToolInvocation({ row }: { row: Extract<TaskChatToolGroupRow, {
}
function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
const { t } = useTranslation("app");
const invocationEntries = getToolInvocationEntries(entries);
const invocationCount = invocationEntries.length;
const errorCount = entries.filter((entry) => entry.type === "tool_error").length;
@@ -346,16 +356,16 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
return (
<details className="task-chat-tool-group" data-testid="task-chat-tool-group">
<summary className="task-chat-tool-group-summary">
<span className="task-chat-tool-group-count">{formatToolCallCount(invocationCount)}</span>
<span className="task-chat-tool-group-count">{formatToolCallCount(invocationCount, t)}</span>
{visibleNames.length > 0 ? (
<span className="task-chat-tool-group-names" aria-label="Tool names">
<span className="task-chat-tool-group-names" aria-label={t("taskChat.toolNames", "Tool names")}>
{visibleNames.join(", ")}
{overflowCount > 0 ? <span className="task-chat-tool-group-overflow">, +{overflowCount} more</span> : null}
{overflowCount > 0 ? <span className="task-chat-tool-group-overflow">{t("taskChat.moreTools", ", +{{count}} more", { count: overflowCount })}</span> : null}
</span>
) : null}
{errorCount > 0 ? (
<span className="task-chat-tool-group-error-count">
{errorCount === 1 ? "1 error" : `${errorCount} errors`}
{t("taskChat.errorCount", "{{count}} error", { count: errorCount })}
</span>
) : null}
</summary>
@@ -373,11 +383,12 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
}
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
const { t } = useTranslation("app");
const combinedThinkingText = entries.map((entry) => entry.text).join("");
return (
<details className="task-chat-thinking" data-testid="task-chat-thinking" open>
<summary className="task-chat-thinking-summary">Thinking</summary>
<summary className="task-chat-thinking-summary">{t("taskChat.thinking", "Thinking")}</summary>
<div className="task-chat-thinking-body">
<div
className="markdown-body task-chat-markdown task-chat-thinking-markdown"
@@ -407,12 +418,13 @@ FNXC:TaskChatTimestamps 2026-06-17-15:43:
FN-6597 requires small relative timestamps on both task-chat agent group headers and user message headers, computed at render time from existing transcript timestamps without adding a live timer.
*/
function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
const { t } = useTranslation("app");
const relativeTime = formatRelativeTimeAgo(message.createdAt);
return (
<section className="task-chat-user-group" aria-label="You message">
<section className="task-chat-user-group" aria-label={t("taskChat.youMessage", "You message")}>
<div className="task-chat-user-header">
<div className="task-chat-role-label">You</div>
<div className="task-chat-role-label">{t("taskChat.you", "You")}</div>
{relativeTime ? (
<span className="task-chat-timestamp" data-testid="task-chat-user-time">
{relativeTime}
@@ -431,6 +443,7 @@ function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
}
export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded }: TaskChatTabProps) {
const { t } = useTranslation("app");
const { entries, loading, loadMore, hasMore, loadingMore } = useAgentLogs(task.id, active, projectId);
const [draft, setDraft] = useState("");
const [sending, setSending] = useState(false);
@@ -452,7 +465,7 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
() => mergeUserMessages(task.steeringComments, optimisticMessages),
[optimisticMessages, task.steeringComments],
);
const transcriptItems = useMemo(() => buildTranscriptItems(entries, userMessages), [entries, userMessages]);
const transcriptItems = useMemo(() => buildTranscriptItems(entries, userMessages, t), [entries, t, userMessages]);
const transcriptItemCount = entries.length + userMessages.length;
const firstEntryKey = entries[0] ? getEntryKey(entries[0], 0) : null;
const activeSession = isActiveAgentSession(task, { sessionLive });
@@ -463,13 +476,13 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
* The task-detail chat must never silently accept a question when no agent session will consume it. Keep idle chats sendable, but surface that the message is saved as guidance for the next task run instead of implying a live reply.
*/
const sessionHint = isDoneTask
? "Send a message to start a refinement task for this completed task."
? t("taskChat.doneSessionHint", "Send a message to start a refinement task for this completed task.")
: activeSession
? "Message the active agent session. Guidance is delivered to the running session in real time."
: "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.";
? t("taskChat.activeSessionHint", "Message the active agent session. Guidance is delivered to the running session in real time.")
: t("taskChat.idleSessionHint", "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.");
const composerPlaceholder = isDoneTask
? "Start a refinement task for this completed task"
: "Steer the currently executing agent";
? t("taskChat.donePlaceholder", "Start a refinement task for this completed task")
: t("taskChat.activePlaceholder", "Steer the currently executing agent");
const canSend = draft.trim().length > 0 && !sending;
const resizeComposer = useCallback(() => {
@@ -711,7 +724,7 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
type="button"
className="btn btn-icon btn-sm task-chat-expand-toggle task-chat-expand-toggle--overlay"
onClick={onToggleExpanded}
aria-label={expanded ? "Collapse chat" : "Expand chat to full modal"}
aria-label={expanded ? t("taskChat.collapseChat", "Collapse chat") : t("taskChat.expandChat", "Expand chat to full modal")}
aria-pressed={expanded}
data-testid="task-chat-expand-toggle"
>
@@ -731,17 +744,17 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
{loadingMore ? (
<div className="task-chat-load-previous-status" role="status" data-testid="task-chat-load-previous-loading">
<Loader2 className="animate-spin" aria-hidden="true" />
<span>Loading earlier messages…</span>
<span>{t("taskChat.loadingEarlierMessages", "Loading earlier messages…")}</span>
</div>
) : (
<button
type="button"
className="btn btn-secondary btn-sm task-chat-load-previous"
onClick={() => { void loadPreviousMessages(); }}
aria-label="Load previous messages"
aria-label={t("taskChat.loadPreviousMessages", "Load previous messages")}
data-testid="task-chat-load-previous"
>
Load previous messages
{t("taskChat.loadPreviousMessages", "Load previous messages")}
</button>
)}
</div>
@@ -749,10 +762,10 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
{loading && transcriptItemCount === 0 ? (
<div className="task-chat-empty" role="status">
<Loader2 className="animate-spin" aria-hidden="true" />
<span>Loading agent output…</span>
<span>{t("taskChat.loadingAgentOutput", "Loading agent output…")}</span>
</div>
) : transcriptItemCount === 0 ? (
<div className="task-chat-empty">No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.</div>
<div className="task-chat-empty">{t("taskChat.emptyAgentOutput", "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.")}</div>
) : (
transcriptItems.map((item, itemIndex) => {
if (item.kind === "user") {
@@ -768,13 +781,13 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
const latestEntryTimestamp = item.entries[item.entries.length - 1]?.timestamp ?? "";
const relativeTime = formatRelativeTimeAgo(latestEntryTimestamp);
return (
<section className="task-chat-group" key={`${item.role ?? "agent"}-${itemIndex}`} aria-label={`${item.label} messages`}>
<section className="task-chat-group" key={`${item.role ?? "agent"}-${itemIndex}`} aria-label={t("taskChat.agentMessages", "{{label}} messages", { label: item.label })}>
<header className="task-chat-group-header">
<AgentAvatar agent={avatarAgent} className="task-chat-avatar" />
<div>
<div className="task-chat-role-label">{item.label}</div>
<div className="task-chat-group-meta">
<span>{item.entries.length === 1 ? "1 entry" : `${item.entries.length} entries`}</span>
<span>{t("taskChat.entryCount", "{{count}} entry", { count: item.entries.length })}</span>
{relativeTime ? (
<span className="task-chat-timestamp" data-testid="task-chat-group-time">
{relativeTime}
@@ -798,11 +811,11 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
type="button"
className="task-chat-jump-to-bottom"
onClick={scrollTranscriptToBottom}
aria-label="Jump to latest message"
aria-label={t("taskChat.jumpToLatestMessage", "Jump to latest message")}
data-testid="task-chat-jump-to-bottom"
>
<ChevronDown aria-hidden="true" />
<span>Latest</span>
<span>{t("taskChat.latest", "Latest")}</span>
</button>
) : null}
</div>
@@ -826,15 +839,15 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
onChange={(event) => setDraft(event.target.value)}
onKeyDown={handleKeyDown}
disabled={sending}
aria-label="Message active agent session"
aria-label={t("taskChat.messageActiveAgentSession", "Message active agent session")}
rows={1}
/>
<button
type="submit"
className="btn btn-primary btn-icon task-chat-send"
disabled={!canSend}
aria-label={sending ? "Sending" : "Send"}
title={sending ? "Sending" : "Send"}
aria-label={sending ? t("taskChat.sending", "Sending") : t("common:actions.send", "Send")}
title={sending ? t("taskChat.sending", "Sending") : t("common:actions.send", "Send")}
>
{sending ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
</button>

View File

@@ -115,7 +115,7 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
<div className="detail-log-header comments-header-row">
<div className="comments-author-row">
{isAIGuidance ? (
<span className="ai-guidance-badge" data-testid="ai-guidance-badge">AI Guidance</span>
<span className="ai-guidance-badge" data-testid="ai-guidance-badge">{t("comments.aiGuidance", "AI Guidance")}</span>
) : (
<strong>{comment.author}</strong>
)}

View File

@@ -71,14 +71,20 @@ interface ModelSelection {
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/;
const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
const STRING_OBJECT_TAG = "[object String]";
function isStringValue(value: unknown): value is string {
return Object.prototype.toString.call(value) === STRING_OBJECT_TAG;
}
const markdownLinkifyComponents: Components = {
p: ({ children, ...props }) => <p {...props}>{linkifyReactChildren(children)}</p>,
li: ({ children, ...props }) => <li {...props}>{linkifyReactChildren(children)}</li>,
code: ({ children, ...props }) => {
const text = typeof children === "string" ? children : React.Children.toArray(children).join("");
const text = React.Children.toArray(children).join(EMPTY_MARKDOWN_CHILD_SEPARATOR);
const linkedChildren = linkifyFilePaths(text);
if (linkedChildren.length === 1 && typeof linkedChildren[0] === "string") {
if (linkedChildren.length === 1 && linkedChildren[0]?.constructor === String) {
return <code {...props}>{children}</code>;
}
return <code {...props}>{linkedChildren}</code>;
@@ -92,25 +98,25 @@ const markdownLinkifyComponents: Components = {
*/
function extractExecutorModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) {
if (entry.agent !== "executor" || entry.type !== "text") continue;
entries.forEach((entry) => {
if (entry.agent !== "executor" || entry.type !== "text") return;
const match = entry.text.match(/^Executor using model: (.+?)\/(.+)$/);
if (match) {
result = { provider: match[1], modelId: match[2] };
}
}
});
return result;
}
function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) {
if (entry.agent !== "reviewer" || entry.type !== "text") continue;
entries.forEach((entry) => {
if (entry.agent !== "reviewer" || entry.type !== "text") return;
const match = entry.text.match(/^Reviewer using model: (.+?)\/(.+)$/);
if (match) {
result = { provider: match[1], modelId: match[2] };
}
}
});
return result;
}
@@ -129,7 +135,7 @@ function hasUsableTrackingTitle(task: { title?: string | null; description?: str
function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSelection {
const runtimeConfig = (agent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined;
const model = typeof runtimeConfig?.model === "string" ? runtimeConfig.model.trim() : "";
const model = isStringValue(runtimeConfig?.model) ? runtimeConfig.model.trim() : "";
if (model) {
const slashIdx = model.indexOf("/");
if (slashIdx > 0 && slashIdx < model.length - 1) {
@@ -140,8 +146,8 @@ function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSele
}
}
const provider = typeof runtimeConfig?.modelProvider === "string" ? runtimeConfig.modelProvider.trim() : "";
const modelId = typeof runtimeConfig?.modelId === "string" ? runtimeConfig.modelId.trim() : "";
const provider = isStringValue(runtimeConfig?.modelProvider) ? runtimeConfig.modelProvider.trim() : "";
const modelId = isStringValue(runtimeConfig?.modelId) ? runtimeConfig.modelId.trim() : "";
return {
provider: provider || undefined,
modelId: modelId || undefined,
@@ -209,13 +215,13 @@ function resolveEffectiveValidator(
function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
// Iterate in chronological order; last match wins
let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) {
if (entry.agent !== "triage" || entry.type !== "text") continue;
entries.forEach((entry) => {
if (entry.agent !== "triage" || entry.type !== "text") return;
const match = entry.text.match(/^Triage using model: (.+?)\/(.+)$/);
if (match) {
result = { provider: match[1], modelId: match[2] };
}
}
});
return result;
}
@@ -431,7 +437,7 @@ function normalizeSourceIssueUrl(value: string): string | undefined {
}
function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority {
return typeof priority === "string" && (TASK_PRIORITIES as readonly string[]).includes(priority)
return isStringValue(priority) && (TASK_PRIORITIES as readonly string[]).includes(priority)
? (priority as TaskPriority)
: DEFAULT_TASK_PRIORITY;
}
@@ -456,7 +462,7 @@ interface ProvenanceLabelOptions {
function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined {
const issueUrl = metadata?.issueUrl;
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
return isStringValue(issueUrl) && issueUrl.length > 0 ? issueUrl : undefined;
}
function parseGithubIssueLabel(url: string): { label: string; href: string } | null {
@@ -474,12 +480,12 @@ function parseGithubIssueLabel(url: string): { label: string; href: string } | n
function getResearchContextInfo(metadata: Task["sourceMetadata"]): string | undefined {
const findingLabel = metadata?.findingLabel;
if (typeof findingLabel === "string" && findingLabel.length > 0) {
if (isStringValue(findingLabel) && findingLabel.length > 0) {
return findingLabel;
}
const runId = metadata?.runId;
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
return isStringValue(runId) && runId.length > 0 ? runId : undefined;
}
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
@@ -659,7 +665,7 @@ export function TaskDetailContent({
(task.stuckKillCount ?? 0) > 0 ||
(task.recoveryRetryCount ?? 0) > 0 ||
Boolean(task.nextRecoveryAt);
const nearDuplicateOf = typeof workingTask.sourceMetadata?.nearDuplicateOf === "string"
const nearDuplicateOf = isStringValue(workingTask.sourceMetadata?.nearDuplicateOf)
? workingTask.sourceMetadata.nearDuplicateOf
: null;
const nearDuplicateCanonical = nearDuplicateOf
@@ -810,11 +816,11 @@ export function TaskDetailContent({
setCustomFieldValues(updated.customFields ?? {});
onTaskUpdated?.(updated);
} catch (err) {
if (err instanceof ApiRequestError && err.details && typeof err.details.fieldId === "string") {
if (err instanceof ApiRequestError && err.details && isStringValue(err.details.fieldId)) {
setCustomFieldError({
code: (err.details.code as CustomFieldRejection["code"]) ?? "type-mismatch",
fieldId: err.details.fieldId,
detail: typeof err.details.detail === "string" ? err.details.detail : err.message,
detail: isStringValue(err.details.detail) ? err.details.detail : err.message,
});
return;
}
@@ -909,7 +915,7 @@ export function TaskDetailContent({
tabId: `plugin-${entry.pluginId}-${index}` as TabId,
}));
const activePluginTab =
typeof activeTab === "string" && activeTab.startsWith("plugin-")
isStringValue(activeTab) && activeTab.startsWith("plugin-")
? pluginTabs.find((tab) => tab.tabId === activeTab) ?? null
: null;
@@ -2981,7 +2987,7 @@ export function TaskDetailContent({
)}
{provenanceDisplay.parentTaskId && (
<>
{" "}of{" "}
{" "}{t("taskDetail.provenance.parentTaskOf", "of")}{" "}
<button
type="button"
className="detail-provenance-link"
@@ -3021,7 +3027,7 @@ export function TaskDetailContent({
<div className="detail-provenance detail-pr-link-row">
<GitBranch aria-hidden="true" />
<span>
PR{" "}
{t("taskDetail.pr.label", "PR")} {" "}
{task.prInfo?.url ? (
<a
className="detail-provenance-link"

View File

@@ -42,6 +42,11 @@ import type { DiscoveredSkill } from "../api";
import type { ToastType } from "../hooks/useToast";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import { useConfirm } from "../hooks/useConfirm";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 localizes this workflow surface through t() and authored en catalog keys so hardcoded user-facing copy does not need a lint.ignore deferral.
*/
import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { useAppSettings } from "../hooks/useAppSettings";
import { isMobileViewport, useViewportMode } from "../hooks/useViewportMode";
@@ -210,6 +215,9 @@ const NOTIFY_EVENT_OPTIONS = [
"workflow-notify",
] as const;
const NOTIFY_CUSTOM_EVENT_VALUE = "__custom";
const WORKFLOW_CLI_COMMAND_PLACEHOLDER = "npm test -- --runInBand";
const WORKFLOW_CODE_SOURCE_PLACEHOLDER = "export default async (ctx) => ({ outcome: \"success\" });";
const WORKFLOW_NOTIFY_MESSAGE_PLACEHOLDER = "Task {{taskId}} reached {{workflowName}}";
const PALETTE: Array<{ kind: WorkflowEditorNodeKind; label: string; icon: typeof MessageSquare; presetConfig?: Record<string, unknown> }> = [
{ kind: "prompt", label: "Prompt", icon: MessageSquare },
@@ -1019,11 +1027,11 @@ function InnerEditor({
return isMobileMode ? null : data[0]?.id ?? null;
});
} catch (err) {
addToast(getErrorMessage(err) || "Failed to load workflows", "error");
addToast(getErrorMessage(err) || t("workflows.loadFailed", "Failed to load workflows"), "error");
} finally {
setLoading(false);
}
}, [projectId, addToast, isMobileMode, initialAction, initialWorkflowId]);
}, [projectId, addToast, isMobileMode, initialAction, initialWorkflowId, t]);
useEffect(() => {
void loadWorkflows();
@@ -1752,11 +1760,11 @@ function InnerEditor({
setWorkflows((ws) => [...ws, created]);
setActiveId(created.id);
setWorkflowListStageOpen(false);
addToast(`Duplicated to "${created.name}" — editable`, "success");
addToast(t("workflows.duplicatedEditable", "Duplicated to \"{{name}}\" — editable", { name: created.name }), "success");
} catch (err) {
addToast(getErrorMessage(err) || "Failed to duplicate workflow", "error");
addToast(getErrorMessage(err) || t("workflows.duplicateFailed", "Failed to duplicate workflow"), "error");
}
}, [activeWorkflow, projectId, addToast]);
}, [activeWorkflow, projectId, addToast, t]);
const handleSave = useCallback(async () => {
if (!activeWorkflow) return;
@@ -2107,25 +2115,25 @@ function InnerEditor({
// step-review offers an optional review model picker (KTD-4).
if (selectedNode?.data.kind === "step-review" && models.length === 0) {
fetchModels().then((res) => setModels(res.models)).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load models", "error");
addToast(getErrorMessage(err) || t("workflowEditor.modelsLoadFailed", "Failed to load models"), "error");
});
return;
}
if (!selectedNode || (selectedNode.data.kind !== "prompt" && selectedNode.data.kind !== "gate")) return;
if (currentExecutor === "model" && models.length === 0) {
fetchModels().then((res) => setModels(res.models)).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load models", "error");
addToast(getErrorMessage(err) || t("workflowEditor.modelsLoadFailed", "Failed to load models"), "error");
});
} else if (currentExecutor === "agent" && agents.length === 0) {
// Project-scoped, matching WorkflowColumnPanel's fetchAgents(undefined,
// projectId) — an unscoped fetch returns the wrong registry in
// multi-project deployments (PR #1432 review).
fetchAgents(undefined, projectId).then(setAgents).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load agents", "error");
addToast(getErrorMessage(err) || t("workflowEditor.agentsLoadFailed", "Failed to load agents"), "error");
});
} else if (currentExecutor === "skill" && skills.length === 0) {
fetchDiscoveredSkills(projectId).then(setSkills).catch((err) => {
addToast(getErrorMessage(err) || "Failed to load skills", "error");
addToast(getErrorMessage(err) || t("workflowEditor.skillsLoadFailed", "Failed to load skills"), "error");
});
}
}, [
@@ -2137,6 +2145,7 @@ function InnerEditor({
models.length,
agents.length,
skills.length,
t,
]);
// ── Dirty-state dismissal guard (U4, R7) ────────────────────────────────────
@@ -2198,12 +2207,12 @@ function InnerEditor({
Promise.resolve(fetchAgents(undefined, projectId)).then((list) => {
if (!cancelled) setAgents(list ?? []);
}).catch((err) => {
if (!cancelled) addToast(getErrorMessage(err) || "Failed to load agents", "error");
if (!cancelled) addToast(getErrorMessage(err) || t("workflowEditor.agentsLoadFailed", "Failed to load agents"), "error");
});
return () => {
cancelled = true;
};
}, [overrideColumnBinding, agents.length, projectId, addToast]);
}, [overrideColumnBinding, agents.length, projectId, addToast, t]);
const overlayProps = useOverlayDismiss(requestClose);
const promptFullscreenOverlay =
@@ -2226,7 +2235,7 @@ function InnerEditor({
</button>
</div>
<label className="wf-field">
<span>Prompt</span>
<span>{t("workflowEditor.prompt", "Prompt")}</span>
<textarea
rows={undefined}
value={selectedNodePromptValue}
@@ -2263,8 +2272,8 @@ function InnerEditor({
}}
>
<header className="wf-editor-header">
<h2>Workflows</h2>
<button className="wf-editor-close" onClick={requestClose} aria-label="Close workflow editor">
<h2>{t("workflows.title", "Workflows")}</h2>
<button className="wf-editor-close" onClick={requestClose} aria-label={t("workflows.closeEditor", "Close workflow editor")}>
<X size={18} />
</button>
</header>
@@ -2349,10 +2358,10 @@ function InnerEditor({
) : null}
{loading ? (
<div className="wf-editor-empty">
<Loader2 size={16} className="wf-spin" /> Loading…
<Loader2 size={16} className="wf-spin" /> {t("workflows.loading", "Loading…")}
</div>
) : workflows.length === 0 ? (
<div className="wf-editor-empty">No workflows yet.</div>
<div className="wf-editor-empty">{t("workflows.noneYet", "No workflows yet.")}</div>
) : (
<ul className="wf-editor-list">
{workflows.map((w) => (
@@ -3236,7 +3245,7 @@ function InnerEditor({
!(compactLayoutEnabled && !isMobileMode) && (
<aside className="wf-editor-inspector" data-testid="wf-node-inspector">
<div className="wf-inspector-heading">
<h3>Node</h3>
<h3>{t("workflowNodes.nodeInspector", "Node")}</h3>
{isMobileMode && (
<button
type="button"
@@ -3258,14 +3267,14 @@ function InnerEditor({
</div>
{isBuiltin && (
<p className="wf-inspector-note wf-inspector-note--info">
Read-only built-in — duplicate the workflow to edit nodes.
{t("workflowNodes.readOnlyDuplicateToEdit", "Read-only built-in — duplicate the workflow to edit nodes.")}
</p>
)}
<fieldset className="wf-inspector-fields" disabled={isBuiltin}>
{/* FNXC:WorkflowEditor 2026-06-17-00:20: Start labels are structural and ignored by flowToIr, so exposing the generic Name editor would create a no-op rename. */}
{selectedNode.data.kind !== "start" && (
<label className="wf-field">
<span>Name</span>
<span>{t("common:labels.name", "Name")}</span>
<input
value={selectedNode.data.label}
onChange={(e) => updateSelectedData({ label: e.target.value })}
@@ -3307,7 +3316,7 @@ function InnerEditor({
{selectedNode.data.kind === "prompt" || selectedNode.data.kind === "gate" ? (
<div className="wf-prompt-editor">
<label className="wf-field">
<span>Prompt</span>
<span>{t("workflowEditor.prompt", "Prompt")}</span>
<textarea
rows={5}
value={selectedNodePromptValue}
@@ -3334,15 +3343,15 @@ function InnerEditor({
{selectedNode.data.kind === "prompt" ? (
<>
<label className="wf-field">
<span>Executor</span>
<span>{t("workflowEditor.executor", "Executor")}</span>
<select
value={currentExecutor}
onChange={(e) => updateSelectedData({ config: { executor: e.target.value } })}
>
<option value="model">Model</option>
<option value="agent">Agent</option>
<option value="skill">Skill</option>
<option value="cli">CLI / script</option>
<option value="model">{t("workflowEditor.model", "Model")}</option>
<option value="agent">{t("workflowEditor.agent", "Agent")}</option>
<option value="skill">{t("workflowEditor.skill", "Skill")}</option>
<option value="cli">{t("workflowEditor.cliScript", "CLI / script")}</option>
<option value="cli-agent">{t("workflowEditor.cliAgent.executorOption")}</option>
</select>
</label>
@@ -3362,9 +3371,9 @@ function InnerEditor({
{currentExecutor === "model" && (
<label className="wf-field">
<span>Model</span>
<span>{t("workflowEditor.model", "Model")}</span>
<CustomModelDropdown
label="Model"
label={t("workflowEditor.model", "Model")}
models={models}
value={getModelDropdownValue(
String(selectedNode.data.config?.modelProvider ?? ""),
@@ -3386,12 +3395,12 @@ function InnerEditor({
const nodeAgentStale = nodeAgentId !== "" && !agents.some((a) => a.id === nodeAgentId);
return (
<label className="wf-field">
<span>Agent</span>
<span>{t("workflowEditor.agent", "Agent")}</span>
<select
value={nodeAgentId}
onChange={(e) => updateSelectedData({ config: { agentId: e.target.value || undefined } })}
>
<option value="">— select agent —</option>
<option value="">{t("workflowEditor.selectAgent", "— select agent —")}</option>
{nodeAgentStale && (
<option value={nodeAgentId}>
{t("workflowColumns.agentNotFound", "Agent not found — {{id}}", { id: nodeAgentId })}
@@ -3412,12 +3421,12 @@ function InnerEditor({
{currentExecutor === "skill" && (
<label className="wf-field">
<span>Skill</span>
<span>{t("workflowEditor.skill", "Skill")}</span>
<select
value={String(selectedNode.data.config?.skillName ?? "")}
onChange={(e) => updateSelectedData({ config: { skillName: e.target.value || undefined } })}
>
<option value="">— select skill —</option>
<option value="">{t("workflowEditor.selectSkill", "— select skill —")}</option>
{skills.map((s) => (
<option key={s.id} value={s.name}>{s.name}</option>
))}
@@ -3428,26 +3437,26 @@ function InnerEditor({
{currentExecutor === "cli" && (
<>
<label className="wf-field">
<span>CLI mode</span>
<span>{t("workflowEditor.cliMode", "CLI mode")}</span>
<select
value={String(selectedNode.data.config?.cliMode ?? "command")}
onChange={(e) => updateSelectedData({ config: { cliMode: e.target.value } })}
>
<option value="command">Command</option>
<option value="script">Named script</option>
<option value="command">{t("workflowEditor.command", "Command")}</option>
<option value="script">{t("workflowEditor.namedScript", "Named script")}</option>
</select>
</label>
{(selectedNode.data.config?.cliMode ?? "command") === "command" ? (
<label className="wf-field">
<span>Command</span>
<span>{t("workflowEditor.command", "Command")}</span>
<textarea
rows={3}
placeholder="npm test -- --runInBand"
placeholder={WORKFLOW_CLI_COMMAND_PLACEHOLDER}
value={String(selectedNode.data.config?.cliCommand ?? "")}
onChange={(e) => updateSelectedData({ config: { cliCommand: e.target.value } })}
/>
<p className="wf-inspector-note wf-inspector-note--info">
Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.
{t("workflowEditor.cliCommandNote", "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.")}
</p>
<label className="wf-field wf-field--checkbox">
<input
@@ -3455,17 +3464,17 @@ function InnerEditor({
checked={selectedNode.data.config?.cliSkipApproval === true}
onChange={(e) => updateSelectedData({ config: { cliSkipApproval: e.target.checked } })}
/>
<span>Skip first-run approval (runs without pausing)</span>
<span>{t("workflowEditor.skipFirstRunApproval", "Skip first-run approval (runs without pausing)")}</span>
</label>
</label>
) : (
<label className="wf-field">
<span>Script name</span>
<span>{t("workflowEditor.scriptName", "Script name")}</span>
<input
value={String(selectedNode.data.config?.scriptName ?? "")}
onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })}
/>
<span className="wf-inspector-note">Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.</span>
<span className="wf-inspector-note">{t("workflowEditor.namedScriptNote", "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.")}</span>
</label>
)}
</>
@@ -3554,16 +3563,16 @@ function InnerEditor({
checked={Boolean(selectedNode.data.config?.autoApprove)}
onChange={(e) => updateSelectedData({ config: { autoApprove: e.target.checked } })}
/>
<span>Auto-approve requests</span>
<span>{t("workflowEditor.autoApproveRequests", "Auto-approve requests")}</span>
</label>
{Boolean(selectedNode.data.config?.autoApprove) && (
<p className="wf-inspector-note">
Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.
{t("workflowEditor.autoApproveRequestsNote", "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.")}
</p>
)}
<label className="wf-field">
<span>Max retries</span>
<span>{t("workflowEditor.maxRetries", "Max retries")}</span>
<input
type="number"
min={1}
@@ -3594,11 +3603,11 @@ function InnerEditor({
checked={Boolean(selectedNode.data.config?.awaitInput)}
onChange={(e) => updateSelectedData({ config: { awaitInput: e.target.checked } })}
/>
<span>Wait for user input</span>
<span>{t("workflowEditor.waitForUserInput", "Wait for user input")}</span>
</label>
{Boolean(selectedNode.data.config?.awaitInput) && (
<p className="wf-inspector-note wf-inspector-note--info">
This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question.
{t("workflowEditor.waitForUserInputNote", "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question.")}
</p>
)}
</>
@@ -3606,7 +3615,7 @@ function InnerEditor({
{selectedNode.data.kind === "script" ? (
<label className="wf-field">
<span>Script name</span>
<span>{t("workflowEditor.scriptName", "Script name")}</span>
<input
value={String(selectedNode.data.config?.scriptName ?? "")}
onChange={(e) => updateSelectedData({ config: { scriptName: e.target.value } })}
@@ -4086,7 +4095,7 @@ function InnerEditor({
className="wf-code-source"
rows={8}
spellCheck={false}
placeholder={"export default async (ctx) => ({ outcome: \"success\" });"}
placeholder={WORKFLOW_CODE_SOURCE_PLACEHOLDER}
value={String(selectedNode.data.config?.source ?? "")}
onChange={(e) => updateSelectedData({ config: { source: e.target.value } })}
/>
@@ -4176,7 +4185,7 @@ function InnerEditor({
<textarea
rows={4}
value={String(selectedNode.data.config?.message ?? "")}
placeholder="Task {{taskId}} reached {{workflowName}}"
placeholder={WORKFLOW_NOTIFY_MESSAGE_PLACEHOLDER}
onChange={(e) => updateSelectedData({ config: { message: e.target.value } })}
/>
</label>
@@ -4299,8 +4308,8 @@ function InnerEditor({
value={String(selectedEdge.data?.condition ?? "success")}
onChange={(e) => updateSelectedEdge({ condition: e.target.value })}
>
<option value="success">success</option>
<option value="failure">failure</option>
<option value="success">{t("workflowNodes.conditionSuccess", "success")}</option>
<option value="failure">{t("workflowNodes.conditionFailure", "failure")}</option>
</select>
</label>
) : (

View File

@@ -2,6 +2,11 @@ import "@xyflow/react/dist/style.css";
import "./WorkflowResultsTab.css";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 requires the workflow/task/setup/PR dashboard cluster to render user-facing copy through locale catalogs so i18n:lint can scan these files again without narrow deferrals.
*/
import { Check, ChevronDown, ChevronRight, ChevronUp, Maximize2, Pencil, X } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -62,10 +67,11 @@ interface WorkflowResultsTabProps {
/** Extract the user-facing question from a workflow-input paused reason.
* Strips the leading "workflow-input:<nodeId>: " prefix if present. */
function parseWorkflowInputQuestion(pausedReason?: string): string {
if (!pausedReason) return "Reply in the comments and unpause the task to continue.";
function parseWorkflowInputQuestion(pausedReason: string | undefined, t: ReturnType<typeof useTranslation>["t"]): string {
const fallback = t("app:workflow.replyInComments", "Reply in the comments and unpause the task to continue.");
if (!pausedReason) return fallback;
const match = /^workflow-input:[^:]+:\s*(.*)$/s.exec(pausedReason);
if (match) return match[1].trim() || "Reply in the comments and unpause the task to continue.";
if (match) return match[1].trim() || fallback;
return pausedReason;
}
@@ -89,15 +95,15 @@ interface WorkflowStepOption {
function getStatusLabel(status: WorkflowStepResult["status"], t: ReturnType<typeof useTranslation>["t"]): string {
switch (status) {
case "passed":
return t("workflow.statusPassed", "Passed");
return t("app:workflow.statusPassed", "Passed");
case "failed":
return t("workflow.statusFailed", "Failed");
return t("app:workflow.statusFailed", "Failed");
case "advisory_failure":
return t("workflow.statusAdvisory", "Advisory failure");
return t("app:workflow.statusAdvisory", "Advisory failure");
case "skipped":
return t("workflow.statusSkipped", "Skipped");
return t("app:workflow.statusSkipped", "Skipped");
case "pending":
return t("workflow.statusRunning", "Running…");
return t("app:workflow.statusRunning", "Running…");
default:
return status;
}
@@ -135,7 +141,7 @@ function phaseBadge(phase: "pre-merge" | "post-merge", id: string, prefix: strin
className={`phase-badge ${phaseClass}`}
data-testid={`${prefix}-${id}`}
>
{phase === "post-merge" ? t("workflow.postMerge", "Post-merge") : t("workflow.preMerge", "Pre-merge")}
{phase === "post-merge" ? t("app:workflow.postMerge", "Post-merge") : t("app:workflow.preMerge", "Pre-merge")}
</span>
);
}
@@ -145,9 +151,9 @@ function getWorkflowName(
workflows: WorkflowDefinition[],
t: ReturnType<typeof useTranslation>["t"],
): string {
if (!selectedWorkflowId) return t("workflow.defaultWorkflow", "Default");
if (!selectedWorkflowId) return t("app:workflow.defaultWorkflow", "Default");
const match = workflows.find((workflow) => workflow.id === selectedWorkflowId);
return match?.name || t("workflow.customWorkflowFallback", "Custom workflow");
return match?.name || t("app:workflow.customWorkflowFallback", "Custom workflow");
}
function getAggregateWorkflowResult(
@@ -155,18 +161,18 @@ function getAggregateWorkflowResult(
t: ReturnType<typeof useTranslation>["t"],
): { label: string; badgeClass: string; testId: string } {
if (results.some((result) => result.status === "failed")) {
return { label: t("workflow.statusFailed", "Failed"), badgeClass: "workflow-result-badge--failed", testId: "failed" };
return { label: t("app:workflow.statusFailed", "Failed"), badgeClass: "workflow-result-badge--failed", testId: "failed" };
}
if (results.some((result) => result.status === "advisory_failure")) {
return { label: t("workflow.statusAdvisory", "Advisory"), badgeClass: "workflow-result-badge--advisory_failure", testId: "advisory" };
return { label: t("app:workflow.aggregateAdvisory", "Advisory"), badgeClass: "workflow-result-badge--advisory_failure", testId: "advisory" };
}
if (results.some((result) => result.status === "pending")) {
return { label: t("workflow.aggregateInProgress", "In progress"), badgeClass: "workflow-result-badge--pending", testId: "pending" };
return { label: t("app:workflow.aggregateInProgress", "In progress"), badgeClass: "workflow-result-badge--pending", testId: "pending" };
}
if (results.length === 0) {
return { label: t("workflow.aggregateNoResults", "No results"), badgeClass: "workflow-result-badge--skipped", testId: "no-results" };
return { label: t("app:workflow.aggregateNoResults", "No results"), badgeClass: "workflow-result-badge--skipped", testId: "no-results" };
}
return { label: t("workflow.aggregateAllPassed", "All passed"), badgeClass: "workflow-result-badge--passed", testId: "passed" };
return { label: t("app:workflow.aggregateAllPassed", "All passed"), badgeClass: "workflow-result-badge--passed", testId: "passed" };
}
function getExecutionPhase(
@@ -177,13 +183,13 @@ function getExecutionPhase(
t: ReturnType<typeof useTranslation>["t"],
): { label: string; badgeClass: string; testId: string } {
if (taskStatus === "awaiting-user-input") {
return { label: t("workflow.executionAwaitingInput", "Awaiting input"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-input" };
return { label: t("app:workflow.executionAwaitingInput", "Awaiting input"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-input" };
}
if (taskStatus === "awaiting-cli-approval") {
return { label: t("workflow.executionAwaitingCliApproval", "Awaiting CLI approval"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-cli-approval" };
return { label: t("app:workflow.executionAwaitingCliApproval", "Awaiting CLI approval"), badgeClass: "workflow-result-badge--pending", testId: "awaiting-cli-approval" };
}
if (taskStatus === "paused" || taskPausedReason) {
return { label: t("workflow.executionPaused", "Paused"), badgeClass: "workflow-result-badge--pending", testId: "paused" };
return { label: t("app:workflow.executionPaused", "Paused"), badgeClass: "workflow-result-badge--pending", testId: "paused" };
}
const pendingResult = results.find((result) => result.status === "pending");
@@ -191,8 +197,8 @@ function getExecutionPhase(
const isPostMerge = (pendingResult.phase || "pre-merge") === "post-merge";
return {
label: isPostMerge
? t("workflow.executionPostMerge", "Post-merge steps running")
: t("workflow.executionPreMerge", "Pre-merge steps running"),
? t("app:workflow.executionPostMerge", "Post-merge steps running")
: t("app:workflow.executionPreMerge", "Pre-merge steps running"),
badgeClass: "workflow-result-badge--pending",
testId: isPostMerge ? "post-merge" : "pre-merge",
};
@@ -200,14 +206,14 @@ function getExecutionPhase(
const hasTerminalResults = results.length > 0 && results.every((result) => ["passed", "failed", "advisory_failure", "skipped"].includes(result.status));
if (hasTerminalResults || taskStatus === "done" || task?.column === "done" || task?.column === "in-review") {
return { label: t("workflow.executionCompleted", "Completed"), badgeClass: "workflow-result-badge--passed", testId: "completed" };
return { label: t("app:workflow.executionCompleted", "Completed"), badgeClass: "workflow-result-badge--passed", testId: "completed" };
}
return { label: t("workflow.executionNotStarted", "Not started"), badgeClass: "workflow-result-badge--pending", testId: "not-started" };
return { label: t("app:workflow.executionNotStarted", "Not started"), badgeClass: "workflow-result-badge--pending", testId: "not-started" };
}
function formatModelValue(selection: { provider?: string; modelId?: string } | undefined): string {
if (!selection?.provider || !selection.modelId) return "Default";
function formatModelValue(selection: { provider?: string; modelId?: string } | undefined, t: ReturnType<typeof useTranslation>["t"]): string {
if (!selection?.provider || !selection.modelId) return t("app:workflow.modelDefault", "Default");
return `${selection.provider}/${selection.modelId}`;
}
@@ -245,7 +251,7 @@ function LiveAgentLogOutput({
if (stepEntries.length === 0) {
return (
<div className="workflow-live-log" data-testid={`workflow-live-log-${stepId}`}>
<div className="workflow-live-log-empty">{t("workflow.waitingForOutput", "Waiting for agent output…")}</div>
<div className="workflow-live-log-empty">{t("app:workflow.waitingForOutput", "Waiting for agent output…")}</div>
</div>
);
}
@@ -570,7 +576,7 @@ export function WorkflowResultsTab({
return {
id: stepId,
name: stepInfo?.name || stepId,
description: stepInfo?.description || t("workflow.stepDefinitionNotFound", "Step definition not found."),
description: stepInfo?.description || t("app:workflow.stepDefinitionNotFound", "Step definition not found."),
phase: stepInfo?.phase || "pre-merge",
} as WorkflowStepOption;
});
@@ -595,7 +601,7 @@ export function WorkflowResultsTab({
<div className="workflow-results-editor" data-testid="workflow-steps-editor">
<div className="workflow-steps-section">
<small className="workflow-steps-description">
{t("workflow.selectStepsDescription", "Select steps to run after task implementation completes")}
{t("app:workflow.selectStepsDescription", "Select steps to run after task implementation completes")}
</small>
<div className="workflow-steps-list">
{workflowStepOptions.map((step) => (
@@ -625,7 +631,7 @@ export function WorkflowResultsTab({
{selectedWorkflowSteps.length > 1 && (
<div className="workflow-step-order" data-testid="workflow-step-order">
<small className="workflow-step-order-label">{t("workflow.executionOrder", "Execution order:")}</small>
<small className="workflow-step-order-label">{t("app:workflow.executionOrder", "Execution order:")}</small>
{selectedWorkflowSteps.map((stepId, index) => {
const stepInfo = workflowStepLookup.get(stepId);
return (
@@ -639,7 +645,7 @@ export function WorkflowResultsTab({
onClick={() => moveWorkflowStepUp(index)}
disabled={index === 0}
data-testid={`workflow-step-move-up-${stepId}`}
title={t("workflow.moveUp", "Move up")}
title={t("app:workflow.moveUp", "Move up")}
>
<ChevronUp />
</button>
@@ -649,7 +655,7 @@ export function WorkflowResultsTab({
onClick={() => moveWorkflowStepDown(index)}
disabled={index === selectedWorkflowSteps.length - 1}
data-testid={`workflow-step-move-down-${stepId}`}
title={t("workflow.moveDown", "Move down")}
title={t("app:workflow.moveDown", "Move down")}
>
<ChevronDown />
</button>
@@ -658,7 +664,7 @@ export function WorkflowResultsTab({
className="btn btn-icon btn-sm"
onClick={() => removeWorkflowStep(stepId)}
data-testid={`workflow-step-remove-${stepId}`}
title={t("workflow.remove", "Remove")}
title={t("app:workflow.remove", "Remove")}
>
<X />
</button>
@@ -677,7 +683,7 @@ export function WorkflowResultsTab({
return (
<div className="workflow-results-loading" data-testid="workflow-results-loading">
<div className="workflow-results-spinner" />
<span>{t("workflow.loadingResults", "Loading workflow results…")}</span>
<span>{t("app:workflow.loadingResults", "Loading workflow results…")}</span>
</div>
);
}
@@ -685,9 +691,9 @@ export function WorkflowResultsTab({
if (!hasResults) {
return (
<div className="workflow-results-empty" data-testid="workflow-results-empty">
<p>{t("workflow.noStepsConfigured", "No workflow steps configured for this task.")}</p>
<p>{t("app:workflow.noStepsConfigured", "No workflow steps configured for this task.")}</p>
<p className="workflow-results-empty-hint">
{t("workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
{t("app:workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
</p>
</div>
);
@@ -699,26 +705,26 @@ export function WorkflowResultsTab({
const skipped = results.filter((r) => r.status === "skipped").length;
const pending = results.filter((r) => r.status === "pending").length;
const summaryParts: string[] = [t("workflow.summaryStepCount", { count: results.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })];
if (passed > 0) summaryParts.push(t("workflow.summaryPassed", "{{count}} passed", { count: passed }));
if (failed > 0) summaryParts.push(t("workflow.summaryFailed", "{{count}} failed", { count: failed }));
if (advisoryFailures.length > 0) summaryParts.push(t("workflow.summaryAdvisory", "{{count}} advisory", { count: advisoryFailures.length }));
if (skipped > 0) summaryParts.push(t("workflow.summarySkipped", "{{count}} skipped", { count: skipped }));
if (pending > 0) summaryParts.push(t("workflow.summaryRunning", "{{count}} running", { count: pending }));
const summaryParts: string[] = [t("app:workflow.summaryStepCount", { count: results.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })];
if (passed > 0) summaryParts.push(t("app:workflow.summaryPassed", "{{count}} passed", { count: passed }));
if (failed > 0) summaryParts.push(t("app:workflow.summaryFailed", "{{count}} failed", { count: failed }));
if (advisoryFailures.length > 0) summaryParts.push(t("app:workflow.summaryAdvisory", "{{count}} advisory", { count: advisoryFailures.length }));
if (skipped > 0) summaryParts.push(t("app:workflow.summarySkipped", "{{count}} skipped", { count: skipped }));
if (pending > 0) summaryParts.push(t("app:workflow.summaryRunning", "{{count}} running", { count: pending }));
return (
<div className="workflow-results-list" data-testid="workflow-results-list">
<div className="workflow-results-summary-bar" data-testid="workflow-results-summary">
{summaryParts.join(t("workflow.summarySeparator", " · "))}
{summaryParts.join(t("app:workflow.summarySeparator", " · "))}
</div>
{advisoryFailures.length > 0 && (
<div className="workflow-polish-notes" data-testid="workflow-polish-notes">
<h4>{t("workflow.polishNotes", "Polish notes")}</h4>
<p>{t("workflow.advisoryExplanation", "Advisory workflow steps flagged non-blocking improvements:")}</p>
<h4>{t("app:workflow.polishNotes", "Polish notes")}</h4>
<p>{t("app:workflow.advisoryExplanation", "Advisory workflow steps flagged non-blocking improvements:")}</p>
<ul>
{advisoryFailures.map((result, index) => (
<li key={`advisory-${result.workflowStepId}-${index}`}>
<strong>{result.workflowStepName}:</strong> {result.output || t("workflow.needsReview", "Needs follow-up review.")}
<strong>{result.workflowStepName}:</strong> {result.output || t("app:workflow.needsReview", "Needs follow-up review.")}
</li>
))}
</ul>
@@ -758,7 +764,7 @@ export function WorkflowResultsTab({
{result.notes && result.status !== "pending" && (
<div className="workflow-result-notes" data-testid={`workflow-result-notes-${result.workflowStepId}`}>
<span className="workflow-result-notes-label">{t("workflow.notes", "Notes:")} </span>
<span className="workflow-result-notes-label">{t("app:workflow.notes", "Notes:")} </span>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{result.notes}
</ReactMarkdown>
@@ -767,7 +773,7 @@ export function WorkflowResultsTab({
<div className="workflow-result-meta">
{result.startedAt && (
<span className="workflow-result-timestamp">{t("workflow.started", "Started:")} {formatTimestamp(result.startedAt)}</span>
<span className="workflow-result-timestamp">{t("app:workflow.started", "Started:")} {formatTimestamp(result.startedAt)}</span>
)}
{result.completedAt && (
<span className="workflow-result-duration">{formatDuration(result.startedAt, result.completedAt)}</span>
@@ -785,14 +791,14 @@ export function WorkflowResultsTab({
) : result.output ? (
<div className="workflow-result-output-section">
<div className="workflow-result-output-header">
<span className="workflow-result-output-label">{t("workflow.output", "Output:")} </span>
<span className="workflow-result-output-label">{t("app:workflow.output", "Output:")} </span>
<button
type="button"
className="btn btn-sm workflow-result-toggle"
onClick={() => toggleOutput(result.workflowStepId)}
data-testid={`workflow-result-toggle-${result.workflowStepId}`}
>
{isExpanded ? t("workflow.hideOutput", "Hide output") : t("workflow.showOutput", "Show output")}
{isExpanded ? t("app:workflow.hideOutput", "Hide output") : t("app:workflow.showOutput", "Show output")}
</button>
{!isExpanded && (
<span
@@ -809,16 +815,16 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-result-mode-toggle"
onClick={() => toggleRenderMode(result.workflowStepId)}
data-testid={`workflow-result-mode-toggle-${result.workflowStepId}`}
title={(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("workflow.switchToPlain", "Switch to plain text") : t("workflow.switchToMarkdown", "Switch to markdown")}
title={(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("app:workflow.switchToPlain", "Switch to plain text") : t("app:workflow.switchToMarkdown", "Switch to markdown")}
>
{(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("workflow.markdown", "Markdown") : t("workflow.plain", "Plain")}
{(renderModes[result.workflowStepId] ?? "markdown") === "markdown" ? t("app:workflow.markdown", "Markdown") : t("app:workflow.plain", "Plain")}
</button>
<button
type="button"
className="btn btn-icon btn-sm workflow-result-expand-toggle"
onClick={() => openExpandedView(result.workflowStepId)}
data-testid={`workflow-result-expand-${result.workflowStepId}`}
title={t("workflow.expandOutput", "Expand output")}
title={t("app:workflow.expandOutput", "Expand output")}
>
<Maximize2 size={12} />
</button>
@@ -858,18 +864,18 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-results-edit-toggle"
onClick={() => setIsEditing((prev) => !prev)}
data-testid="workflow-steps-edit-toggle"
aria-label={isEditing ? t("workflow.doneEditingAriaLabel", "Done editing workflow steps") : t("workflow.editAriaLabel", "Edit workflow steps")}
title={isEditing ? t("workflow.done", "Done") : t("workflow.edit", "Edit")}
aria-label={isEditing ? t("app:workflow.doneEditingAriaLabel", "Done editing workflow steps") : t("app:workflow.editAriaLabel", "Edit workflow steps")}
title={isEditing ? t("app:workflow.done", "Done") : t("app:workflow.edit", "Edit")}
>
{isEditing ? (
<>
<Check size={14} />
{t("workflow.done", "Done")}
{t("app:workflow.done", "Done")}
</>
) : (
<>
<Pencil size={14} />
{t("workflow.edit", "Edit")}
{t("app:workflow.edit", "Edit")}
</>
)}
</button>
@@ -891,7 +897,7 @@ export function WorkflowResultsTab({
setInputText("");
setSubmitted(true);
} catch (err) {
setResumeError(getErrorMessage(err) || "Failed to resume task");
setResumeError(getErrorMessage(err) || t("app:workflow.resumeTaskError", "Failed to resume task"));
} finally {
setSubmitting(false);
}
@@ -905,7 +911,7 @@ export function WorkflowResultsTab({
await approveTaskWorkflowCli(taskId, projectId);
setSubmitted(true);
} catch (err) {
setResumeError(getErrorMessage(err) || "Failed to approve command");
setResumeError(getErrorMessage(err) || t("app:workflow.approveCommandError", "Failed to approve command"));
} finally {
setSubmitting(false);
}
@@ -915,16 +921,16 @@ export function WorkflowResultsTab({
<div className="workflow-results-tab" data-task-id={taskId}>
{isAwaitingInput && (
<div className="workflow-input-banner" role="alert">
<strong>Waiting for your input</strong>
<span>{parseWorkflowInputQuestion(taskPausedReason)}</span>
<strong>{t("app:workflow.awaitingInputTitle", "Waiting for your input")}</strong>
<span>{parseWorkflowInputQuestion(taskPausedReason, t)}</span>
{submitted ? (
<span className="workflow-input-resuming">Resuming…</span>
<span className="workflow-input-resuming">{t("app:workflow.resuming", "Resuming…")}</span>
) : (
<div className="workflow-input-actions">
<textarea
className="workflow-input-textarea"
rows={3}
placeholder="Type your reply…"
placeholder={t("app:workflow.inputPlaceholder", "Type your reply…")}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
disabled={submitting}
@@ -935,7 +941,7 @@ export function WorkflowResultsTab({
onClick={handleSubmitInput}
disabled={submitting || !inputText.trim()}
>
{submitting ? "Submitting…" : "Submit & resume"}
{submitting ? t("app:workflow.submitting", "Submitting…") : t("app:workflow.submitAndResume", "Submit & resume")}
</button>
{resumeError && (
<span className="workflow-input-error" role="alert">{resumeError}</span>
@@ -946,13 +952,13 @@ export function WorkflowResultsTab({
)}
{isAwaitingCliApproval && (
<div className="workflow-input-banner workflow-input-banner--approval" role="alert">
<strong>Approve CLI command?</strong>
<strong>{t("app:workflow.cliApprovalTitle", "Approve CLI command?")}</strong>
<span className="workflow-input-approval-warning">
This command will run in the task worktree. Approving trusts this exact command for future runs.
{t("app:workflow.cliApprovalWarning", "This command will run in the task worktree. Approving trusts this exact command for future runs.")}
</span>
<pre className="workflow-input-approval-command"><code>{parseCliApprovalCommand(taskPausedReason)}</code></pre>
{submitted ? (
<span className="workflow-input-resuming">Resuming…</span>
<span className="workflow-input-resuming">{t("app:workflow.resuming", "Resuming…")}</span>
) : (
<div className="workflow-input-actions">
<button
@@ -961,9 +967,9 @@ export function WorkflowResultsTab({
onClick={handleApproveCli}
disabled={submitting}
>
{submitting ? "Approving…" : "Approve & run"}
{submitting ? t("app:workflow.approving", "Approving…") : t("app:workflow.approveAndRun", "Approve & run")}
</button>
<span className="workflow-input-keep-paused">To reject, keep the task paused and do not approve.</span>
<span className="workflow-input-keep-paused">{t("app:workflow.cliApprovalRejectHint", "To reject, keep the task paused and do not approve.")}</span>
{resumeError && (
<span className="workflow-input-error" role="alert">{resumeError}</span>
)}
@@ -973,28 +979,28 @@ export function WorkflowResultsTab({
)}
<section className="card workflow-state-summary" data-testid="workflow-state-summary">
<div className="workflow-state-summary__header">
<h4>{t("workflow.overview", "Workflow overview")}</h4>
<h4>{t("app:workflow.overview", "Workflow overview")}</h4>
</div>
<div className="workflow-state-summary__grid">
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-name">
<span className="workflow-state-summary__label">{t("workflow.workflowName", "Workflow")}</span>
<span className="workflow-state-summary__label">{t("app:workflow.workflowName", "Workflow")}</span>
<span className="workflow-state-summary__value">{workflowName}</span>
</div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-phase">
<span className="workflow-state-summary__label">{t("workflow.executionPhase", "Execution phase")}</span>
<span className="workflow-state-summary__label">{t("app:workflow.executionPhase", "Execution phase")}</span>
<span className={`workflow-result-badge ${executionPhase.badgeClass}`} data-testid={`workflow-phase-badge-${executionPhase.testId}`}>
{executionPhase.label}
</span>
</div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-aggregate">
<span className="workflow-state-summary__label">{t("workflow.aggregateResult", "Aggregate result")}</span>
<span className="workflow-state-summary__label">{t("app:workflow.aggregateResult", "Aggregate result")}</span>
<span className={`workflow-result-badge ${aggregateResult.badgeClass}`} data-testid={`workflow-aggregate-badge-${aggregateResult.testId}`}>
{aggregateResult.label}
</span>
</div>
<div className="workflow-state-summary__item" data-testid="workflow-state-summary-count">
<span className="workflow-state-summary__label">{t("workflow.stepProgress", "Step count")}</span>
<span className="workflow-state-summary__value">{t("workflow.stepProgressValue", "{{completed}} of {{total}} steps completed", { completed: completedStepCount, total: results.length })}</span>
<span className="workflow-state-summary__label">{t("app:workflow.stepProgress", "Step count")}</span>
<span className="workflow-state-summary__value">{t("app:workflow.stepProgressValue", "{{completed}} of {{total}} steps completed", { completed: completedStepCount, total: results.length })}</span>
</div>
</div>
</section>
@@ -1008,19 +1014,19 @@ export function WorkflowResultsTab({
>
<span className="workflow-disclosure__title">
{graphExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />}
{t("workflow.graph", "Workflow graph")}
{t("app:workflow.graph", "Workflow graph")}
</span>
</button>
{graphExpanded && (
<div className="workflow-disclosure__content">
{!selectedWorkflowId ? (
<p className="workflow-disclosure__empty" data-testid="workflow-graph-empty">
{t("workflow.noWorkflowAssigned", "No workflow assigned")}
{t("app:workflow.noWorkflowAssigned", "No workflow assigned")}
</p>
) : workflowGraphLoading && !graphWorkflow ? (
<div className="workflow-results-loading" data-testid="workflow-graph-loading">
<div className="workflow-results-spinner" />
<span>{t("workflow.loadingGraph", "Loading workflow graph…")}</span>
<span>{t("app:workflow.loadingGraph", "Loading workflow graph…")}</span>
</div>
) : graphFlow ? (
<div className="workflow-graph-preview" data-testid="workflow-graph-preview">
@@ -1042,7 +1048,7 @@ export function WorkflowResultsTab({
</div>
) : (
<p className="workflow-disclosure__empty" data-testid="workflow-graph-unavailable">
{t("workflow.graphUnavailable", "Workflow graph unavailable")}
{t("app:workflow.graphUnavailable", "Workflow graph unavailable")}
</p>
)}
</div>
@@ -1051,7 +1057,7 @@ export function WorkflowResultsTab({
<section className="card workflow-management" data-testid="workflow-management-section">
<div className="workflow-management__header">
<h4>{t("workflow.workflowName", "Workflow")}</h4>
<h4>{t("app:workflow.workflowName", "Workflow")}</h4>
{canEdit && selectedWorkflowId && onEditWorkflow && (
<button
type="button"
@@ -1060,7 +1066,7 @@ export function WorkflowResultsTab({
data-testid="workflow-edit-button"
>
<Pencil aria-hidden />
{t("workflow.editWorkflow", "Edit workflow")}
{t("app:workflow.editWorkflow", "Edit workflow")}
</button>
)}
</div>
@@ -1068,7 +1074,7 @@ export function WorkflowResultsTab({
value={selectedWorkflowId}
onChange={handleWorkflowSelect}
projectId={projectId}
label="Custom workflow"
label={t("app:workflow.customWorkflowLabel", "Custom workflow")}
disabled={!canEdit}
/>
</section>
@@ -1082,16 +1088,16 @@ export function WorkflowResultsTab({
>
<span className="workflow-disclosure__title">
{modelSettingsExpanded ? <ChevronDown aria-hidden /> : <ChevronRight aria-hidden />}
{t("workflow.modelSettings", "Model settings")}
{t("app:workflow.modelSettings", "Model settings")}
</span>
</button>
{modelSettingsExpanded && (
<div className="workflow-disclosure__content workflow-state-summary__grid" data-testid="workflow-model-settings-content">
{[
{ key: "executor", label: t("models.targetLabels.executor", "Executor"), value: formatModelValue(effectiveExecutor), provider: effectiveExecutor?.provider },
{ key: "reviewer", label: t("models.targetLabels.validator", "Reviewer"), value: formatModelValue(effectiveValidator), provider: effectiveValidator?.provider },
{ key: "planning", label: t("models.targetLabels.planning", "Planning"), value: formatModelValue(effectivePlanning), provider: effectivePlanning?.provider },
{ key: "thinking", label: t("workflow.thinkingLevel", "Thinking level"), value: task?.thinkingLevel || "Default" },
{ key: "executor", label: t("models.targetLabels.executor", "Executor"), value: formatModelValue(effectiveExecutor, t), provider: effectiveExecutor?.provider },
{ key: "reviewer", label: t("models.targetLabels.validator", "Reviewer"), value: formatModelValue(effectiveValidator, t), provider: effectiveValidator?.provider },
{ key: "planning", label: t("models.targetLabels.planning", "Planning"), value: formatModelValue(effectivePlanning, t), provider: effectivePlanning?.provider },
{ key: "thinking", label: t("app:workflow.thinkingLevel", "Thinking level"), value: task?.thinkingLevel || "Default" },
].map((item) => (
<div className="workflow-state-summary__item" key={item.key} data-testid={`workflow-model-setting-${item.key}`}>
<span className="workflow-state-summary__label">{item.label}</span>
@@ -1108,9 +1114,9 @@ export function WorkflowResultsTab({
<div className="workflow-configured-steps" data-testid="workflow-configured-steps">
<div className="workflow-configured-header" data-testid="workflow-configured-header">
<div className="workflow-configured-title-row">
<h4>{t("workflow.configuredSteps", "Configured Workflow Steps")}</h4>
<h4>{t("app:workflow.configuredSteps", "Configured Workflow Steps")}</h4>
<span className="workflow-configured-count" data-testid="workflow-configured-count">
{t("workflow.stepCount", { count: configuredSteps.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })}
{t("app:workflow.stepCount", { count: configuredSteps.length, defaultValue_one: "{{count}} step", defaultValue_other: "{{count}} steps" })}
</span>
</div>
{editButton}
@@ -1133,7 +1139,7 @@ export function WorkflowResultsTab({
</div>
<p className="workflow-results-empty-hint">
{t("workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
{t("app:workflow.stepsExplanation", "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")}
</p>
{renderEditor()}
@@ -1142,7 +1148,7 @@ export function WorkflowResultsTab({
<>
{showEditHeaderForResults && (
<div className="workflow-results-edit-header" data-testid="workflow-results-edit-header">
<h4>{t("workflow.steps", "Workflow Steps")}</h4>
<h4>{t("app:workflow.steps", "Workflow Steps")}</h4>
{editButton}
</div>
)}
@@ -1179,9 +1185,9 @@ export function WorkflowResultsTab({
className="btn btn-sm workflow-result-mode-toggle"
onClick={() => toggleRenderMode(result.workflowStepId)}
data-testid="workflow-output-modal-mode-toggle"
title={renderMode === "markdown" ? t("workflow.switchToPlain", "Switch to plain text") : t("workflow.switchToMarkdown", "Switch to markdown")}
title={renderMode === "markdown" ? t("app:workflow.switchToPlain", "Switch to plain text") : t("app:workflow.switchToMarkdown", "Switch to markdown")}
>
{renderMode === "markdown" ? t("workflow.markdown", "Markdown") : t("workflow.plain", "Plain")}
{renderMode === "markdown" ? t("app:workflow.markdown", "Markdown") : t("app:workflow.plain", "Plain")}
</button>
<button
type="button"

View File

@@ -8,6 +8,11 @@ import { fetchWorkflow, fetchWorkflows, fetchProjectDefaultWorkflow, setProjectD
import type { ToastType } from "../hooks/useToast";
import { useConfirm } from "../hooks/useConfirm";
/*
FNXC:i18n-Localize 2026-06-20-00:00:
FN-6770 localizes this workflow surface through t() and authored en catalog keys so hardcoded user-facing copy does not need a lint.ignore deferral.
*/
interface WorkflowSelectorProps {
/** Currently selected workflow id, or null for none. */
value: string | null;
@@ -63,7 +68,7 @@ export function WorkflowSelector({
})
.catch((err) => {
if (!cancelled) setWorkflows([]);
addToast?.(getErrorMessage(err) || "Failed to load workflows", "error");
addToast?.(getErrorMessage(err) || t("workflowSelector.loadFailed", "Failed to load workflows"), "error");
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -93,7 +98,7 @@ export function WorkflowSelector({
try {
await onChange(workflowId);
} catch (err) {
addToast?.(getErrorMessage(err) || "Failed to apply workflow", "error");
addToast?.(getErrorMessage(err) || t("workflowSelector.applyFailed", "Failed to apply workflow"), "error");
} finally {
setApplying(false);
}
@@ -115,7 +120,7 @@ export function WorkflowSelector({
disabled={disabled || loading || applying}
onChange={(e) => void handleChange(e.target.value)}
>
<option value="">None</option>
<option value="">{t("workflowSelector.none", "None")}</option>
{workflows.map((w) => (
<option key={w.id} value={w.id}>
{w.name}
@@ -125,7 +130,7 @@ export function WorkflowSelector({
</div>
{onManage && (
<button type="button" className="workflow-selector-manage" onClick={onManage}>
Manage…
{t("workflowSelector.manage", "Manage…")}
</button>
)}
</div>
@@ -140,6 +145,7 @@ interface ProjectDefaultWorkflowFieldProps {
/** Self-contained project-default workflow picker for the settings modal. */
export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: ProjectDefaultWorkflowFieldProps) {
const { t } = useTranslation("app");
const [value, setValue] = useState<string | null>(null);
useEffect(() => {
@@ -162,9 +168,9 @@ export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: P
async (workflowId: string | null) => {
const res = await setProjectDefaultWorkflow(workflowId, projectId);
setValue(res.workflowId);
addToast?.(workflowId ? "Default workflow set" : "Default workflow cleared", "success");
addToast?.(workflowId ? t("workflowSelector.defaultSet", "Default workflow set") : t("workflowSelector.defaultCleared", "Default workflow cleared"), "success");
},
[projectId, addToast],
[projectId, addToast, t],
);
return (
@@ -173,7 +179,7 @@ export function ProjectDefaultWorkflowField({ projectId, addToast, onManage }: P
onChange={handleChange}
projectId={projectId}
addToast={addToast}
label="Default workflow for new tasks"
label={t("workflowSelector.defaultWorkflowLabel", "Default workflow for new tasks")}
onManage={onManage}
/>
);

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
},
"todo": "To Do",
"triage": "Triage"
"triage": "Triage",
"workflow": {
"edit": "Edit workflows",
"new": "New workflow"
}
},
"branchGroup": {
"abandonGroup": "Abandon group",
@@ -1810,7 +1814,8 @@
"heading": "Comments",
"placeholder": "Add a comment",
"postingButton": "Posting…",
"updatedSuccess": "Comment updated"
"updatedSuccess": "Comment updated",
"aiGuidance": "AI Guidance"
},
"commit": {
"filesChanged_one": "Files Changed ({{count}})",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Re-run preflight",
"revertToAi": "Revert to AI version",
"titleLabel": "Title",
"usingTemplate": "Using <code>.github/pull_request_template.md</code>"
"usingTemplate": "Using <code>.github/pull_request_template.md</code>",
"assignees": "Assignees",
"dismissConflictResolutionError": "Dismiss conflict resolution error",
"dismissPushBranchError": "Dismiss push branch error",
"error": {
"actionOpen": "Action: open",
"actionRun": "Action: run",
"docs": "docs"
},
"generatingBody": "Generating AI body…",
"generatingTitle": "Generating AI title…",
"labels": "Labels",
"loadingOptions": "Loading PR options…",
"loadingPreflight": "Loading pre-flight checks…",
"noChangedFiles": "No changed files detected.",
"noCommits": "No commits found.",
"pushBranch": {
"button": "Push branch to remote",
"message": "Fusion will push this task's branch to origin so the PR can be created.",
"title": "Push branch to remote"
},
"resolveConflicts": {
"button": "Resolve conflicts with AI",
"message": "Fusion will use AI to resolve conflicts on this branch and push it.",
"title": "Resolve conflicts with AI"
},
"reviewers": "Reviewers",
"view": {
"agentDisagreed": "agent disagreed",
"agentReplyFix": "Agent reply — fix {{sha}}",
"approve": "Approve",
"autoMerge": "Auto-merge",
"close": "Close",
"confirmMerge": "Confirm merge",
"creating": "Creating PR…",
"creationFailed": "PR creation failed",
"loading": "Loading PR…",
"merge": "Merge",
"mergeableLabel": "Mergeable:",
"mergeDisabledUntilVerified": "Merge is disabled until GitHub verifies this PR",
"none": "none",
"noReviewThreads": "No review threads.",
"requestRetry": "Request retry",
"resolveConflictsBeforeMerge": "Resolve conflicts on GitHub before merging",
"resolveConflictsOnGithub": "Resolve conflicts on GitHub",
"responseAlreadyInProgress": "A response run is already in progress",
"responsePending_one": "Response run in progress — {{count}} threads pending",
"responsePending_other": "Response run in progress — {{count}} threads pending",
"retryCreation": "Retry PR creation",
"reviewLabel": "Review:",
"threadFixed": "fixed",
"threadPending": "pending",
"verifyingGithub": "Verifying with GitHub…"
}
},
"preview": {
"blockedDescription": "You can view the preview in a separate browser tab.",
@@ -5553,7 +5611,8 @@
"totalSize": "total size",
"view": "View ",
"whenEnabledProjectAndAgentMemoryFilesAre": "When enabled, project and agent memory files are backed up automatically on a schedule.",
"whenEnabledTheDatabaseIsBackedUpAutomatically": "When enabled, the database is backed up automatically on a schedule"
"whenEnabledTheDatabaseIsBackedUpAutomatically": "When enabled, the database is backed up automatically on a schedule",
"createFailed": "Failed to create backup"
},
"clearToDefault": "Reset to default",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "Help",
"version": "Version {{version}}"
"version": "Version {{version}}",
"checkUpdates": "Check for updates",
"helpDiscussions": "Help and discussions"
},
"general": {
"25": "25",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "Join our Discord",
"star": "Star",
"starFusion": "Star Fusion on GitHub"
},
"importExport": {
"confirmImport": "Confirm Import",
@@ -5756,7 +5820,35 @@
"importing": "Importing…",
"importTitle": "Import Settings",
"loadingFile": "Loading…",
"reviewPrompt": "Review the settings to be imported:"
"reviewPrompt": "Review the settings to be imported:",
"counts": {
"global_one": "{{count}} global",
"global_other": "{{count}} global",
"project_one": "{{count}} project",
"project_other": "{{count}} project",
"workflowSettings_one": "{{count}} workflow setting value",
"workflowSettings_other": "{{count}} workflow setting value"
},
"exported": "Settings exported ({{scope}} scope)",
"exportFailed": "Failed to export settings",
"globalSettings": "Global Settings:",
"imported": "Imported {{counts}} setting(s)",
"importFailed": "Import failed",
"importFailedDetailed": "Failed to import settings",
"importScope": "Import Scope:",
"importTitleAttr": "Import settings from JSON file",
"invalidJson": "Invalid JSON file: {{error}}",
"mergeExisting": "Merge with existing settings (recommended)",
"projectSettings": "Project Settings:",
"replaceWarning": "If unchecked, existing settings will be replaced with imported values.",
"scopeBoth": "Both global and project settings",
"scopeGlobal": "Global settings only",
"scopeLabel": {
"all": "all",
"global": "global",
"project": "project"
},
"scopeProject": "Project settings only"
},
"jsonPlaceholder": "Enter JSON value...",
"keepLocal": "Keep Local",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "Search memory with qmd",
"testing": "Testing…",
"testRetrieval": "Test Retrieval",
"turnsDailyNotesIntoDREAMSMdAndPromotes": "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md."
"turnsDailyNotesIntoDREAMSMdAndPromotes": "Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md.",
"qmdInstalled": "qmd installed successfully",
"qmdInstallFailed": "Failed to install qmd",
"qmdInstallUnavailable": "qmd install finished, but qmd is still unavailable"
},
"merge": {
"abort": "Abort",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
"useExisting": "Use Existing",
"usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed."
"usingQuickTunnel": "Using Quick Tunnel — automatically creates a random trycloudflare.com URL, no account needed.",
"installationFailed": "Installation failed"
},
"researchGlobal": {
"advancedExternalSearchProviders": "Advanced — external search providers",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "Timeout in minutes for detecting stuck tasks. When a task&apos;s agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.",
"whenEnabledTasksThatModifyTheSameFiles": "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts",
"whenEnabledTasksWithStalePlansPROMPTMd": "When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning",
"whenTheStuckDetectorKillsAndReQueues": "When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled."
"whenTheStuckDetectorKillsAndReQueues": "When the stuck detector kills and re-queues a task, keep completed step statuses so the agent can resume from where it left off. Disable to reset every step to pending on each stuck retry. Default: enabled.",
"browseWorkspacePath": "Browse workspace path",
"overlapPickerNote": "Choose a file to ignore directly, or navigate into a folder and select the current directory."
},
"scope": {
"globalBanner": "These settings are shared across all your Fusion projects.",
@@ -6335,7 +6433,12 @@
"worktrunk": " worktrunk ",
"worktrunkBinaryPath": "Worktrunk binary path",
"worktrunkFailureBehavior": "Worktrunk failure behavior",
"worktrunkIntegration": "Worktrunk integration"
"worktrunkIntegration": "Worktrunk integration",
"worktreesPickerNote": "Navigate to the folder where Fusion should create task worktrees, then select the current directory."
},
"fileBrowser": {
"currentDirectory": "Current directory:",
"projectRoot": "(project root)"
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Create tasks manually",
"withoutGitHub2": "Describe work for AI agents",
"withoutGitHub3": "Track progress on the board",
"withoutGitHubHeading": "Without GitHub (available now):"
"withoutGitHubHeading": "Without GitHub (available now):",
"brandLogo": "Fusion logo",
"brandName": "Fusion",
"setupCompleteTitle": "Setup Complete!"
},
"shell": {
"activePill": "Active",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Merging fixes…",
"mergingPr": "Merging PR…",
"startPrReview": "Start PR Review",
"statusRefreshed": "PR status refreshed"
"statusRefreshed": "PR status refreshed",
"label": "PR"
},
"priority": {
"ariaLabel": "Task priority",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "Created by",
"createdVia": "Created via"
"createdVia": "Created via",
"parentTaskOf": "of"
},
"recoveryState": "Recovery state",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Switch to markdown",
"switchToPlain": "Switch to plain text",
"thinkingLevel": "Thinking level",
"workflowName": "Workflow"
"workflowName": "Workflow",
"aggregateAdvisory": "Advisory",
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"approveAndRun": "Approve & run",
"approveCommandError": "Failed to approve command",
"approving": "Approving…",
"awaitingInputTitle": "Waiting for your input",
"cliApprovalRejectHint": "To reject, keep the task paused and do not approve.",
"cliApprovalTitle": "Approve CLI command?",
"cliApprovalWarning": "This command will run in the task worktree. Approving trusts this exact command for future runs.",
"customWorkflowFallback": "Custom workflow",
"customWorkflowLabel": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"inputPlaceholder": "Type your reply…",
"modelDefault": "Default",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"replyInComments": "Reply in the comments and unpause the task to continue.",
"resumeTaskError": "Failed to resume task",
"resuming": "Resuming…",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"submitAndResume": "Submit & resume",
"submitting": "Submitting…",
"waitingForOutput": "Waiting for agent output…"
},
"workflowColumns": {
"add": "Add column",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "Collapse prompt editor",
"editingPrompt": "Editing Prompt",
"expandPrompt": "Expand prompt editor"
"expandPrompt": "Expand prompt editor",
"agent": "Agent",
"agentsLoadFailed": "Failed to load agents",
"autoApproveRequests": "Auto-approve requests",
"autoApproveRequestsNote": "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.",
"cliCommandNote": "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.",
"cliMode": "CLI mode",
"cliScript": "CLI / script",
"command": "Command",
"executor": "Executor",
"maxRetries": "Max retries",
"model": "Model",
"modelsLoadFailed": "Failed to load models",
"namedScript": "Named script",
"namedScriptNote": "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.",
"prompt": "Prompt",
"scriptName": "Script name",
"selectAgent": "— select agent —",
"selectSkill": "— select skill —",
"skill": "Skill",
"skillsLoadFailed": "Failed to load skills",
"skipFirstRunApproval": "Skip first-run approval (runs without pausing)",
"waitForUserInput": "Wait for user input",
"waitForUserInputNote": "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question."
},
"workflowFields": {
"add": "Add field",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Plugin steps",
"templatesSection": "Templates",
"timeoutMs": "{{timeout}}ms",
"trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out."
"trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out.",
"conditionFailure": "failure",
"conditionSuccess": "success",
"nodeInspector": "Node",
"readOnlyDuplicateToEdit": "Read-only built-in — duplicate the workflow to edit nodes."
},
"workflows": {
"aiEdit": "Design with AI",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nodes",
"templatePickerLabel": "Start from",
"templateSectionBuiltin": "Built-in workflows",
"templateSectionYours": "Your workflows"
"templateSectionYours": "Your workflows",
"closeEditor": "Close workflow editor",
"duplicatedEditable": "Duplicated to \"{{name}}\" — editable",
"duplicateFailed": "Failed to duplicate workflow",
"loadFailed": "Failed to load workflows",
"loading": "Loading…",
"noneYet": "No workflows yet.",
"title": "Workflows"
},
"workflowSelector": {
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
"switchActiveTitle": "Switch workflow?",
"switchCancel": "Cancel",
"switchConfirm": "Switch and abort"
"switchConfirm": "Switch and abort",
"applyFailed": "Failed to apply workflow",
"defaultCleared": "Default workflow cleared",
"defaultSet": "Default workflow set",
"defaultWorkflowLabel": "Default workflow for new tasks",
"loadFailed": "Failed to load workflows",
"manage": "Manage…",
"none": "None"
},
"workflowSettings": {
"add": "Add setting",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk install request",
"sha256": "SHA-256",
"version": "Version"
},
"taskChat": {
"activePlaceholder": "Steer the currently executing agent",
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
"agentMessages": "{{label}} messages",
"arguments": "Arguments",
"collapseChat": "Collapse chat",
"donePlaceholder": "Start a refinement task for this completed task",
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
"entryCount_one": "{{count}} entry",
"entryCount_other": "{{count}} entries",
"error": "Error",
"errorCount_one": "{{count}} error",
"errorCount_other": "{{count}} errors",
"expandChat": "Expand chat to full modal",
"idleSessionHint": "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.",
"jumpToLatestMessage": "Jump to latest message",
"latest": "Latest",
"loadingAgentOutput": "Loading agent output…",
"loadingEarlierMessages": "Loading earlier messages…",
"loadPreviousMessages": "Load previous messages",
"message": "Message",
"messageActiveAgentSession": "Message active agent session",
"moreTools_one": ", +{{count}} more",
"moreTools_other": ", +{{count}} more",
"result": "Result",
"roles": {
"agent": "Agent",
"executor": "Executor",
"merger": "Merger",
"planner": "Planner",
"reviewer": "Reviewer"
},
"sending": "Sending",
"thinking": "Thinking",
"toolCall": "Tool call",
"toolCallCount_one": "{{count}} tool call",
"toolCallCount_other": "{{count}} tool calls",
"toolCallTo": "Tool call → {{label}}",
"toolError": "Tool error",
"toolNames": "Tool names",
"toolResult": "Tool result",
"you": "You",
"youMessage": "You message"
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": "Send"
},
"agents": {
"ratings": {
"trendDeclining": "↓ Declining",
@@ -78,6 +81,9 @@
"offline": "Offline",
"online": "Online"
},
"labels": {
"name": "Name"
},
"merge": {
"unknown": "Unknown"
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual"
},
"workflow": {
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"customWorkflowFallback": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": {
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": ""
},
"todo": "Por hacer",
"triage": "Triaje"
"triage": "Triaje",
"workflow": {
"edit": "",
"new": ""
}
},
"branchGroup": {
"abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "Comentarios",
"placeholder": "Añadir un comentario",
"postingButton": "Publicando…",
"updatedSuccess": "Comentario actualizado"
"updatedSuccess": "Comentario actualizado",
"aiGuidance": ""
},
"commit": {
"filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Volver a ejecutar la comprobación previa",
"revertToAi": "Revertir a la versión de IA",
"titleLabel": "Título",
"usingTemplate": "Usando <code>.github/pull_request_template.md</code>"
"usingTemplate": "Usando <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
},
"preview": {
"blockedDescription": "Puede ver la vista previa en una pestaña del navegador separada.",
@@ -5553,7 +5611,8 @@
"totalSize": "",
"view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
"whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
},
"clearToDefault": "",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "Ayuda",
"version": "Versión {{version}}"
"version": "Versión {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
},
"general": {
"25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
},
"importExport": {
"confirmImport": "Confirmar importación",
@@ -5756,7 +5820,35 @@
"importing": "Importando…",
"importTitle": "Importar configuración",
"loadingFile": "Cargando…",
"reviewPrompt": "Revisa la configuración a importar:"
"reviewPrompt": "Revisa la configuración a importar:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
},
"jsonPlaceholder": "Ingresa un valor JSON...",
"keepLocal": "Mantener local",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "",
"testing": "Probando…",
"testRetrieval": "Probar recuperación",
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
"turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
},
"merge": {
"abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "Usar existente",
"usingQuickTunnel": ""
"usingQuickTunnel": "",
"installationFailed": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": ""
"whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
},
"scope": {
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
@@ -6335,7 +6433,12 @@
"worktrunk": "",
"worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "",
"worktrunkIntegration": ""
"worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Crear tareas manualmente",
"withoutGitHub2": "Describir trabajo para agentes de IA",
"withoutGitHub3": "Seguir el progreso en el tablero",
"withoutGitHubHeading": "Sin GitHub (disponible ahora):"
"withoutGitHubHeading": "Sin GitHub (disponible ahora):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
},
"shell": {
"activePill": "Activo",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Fusionando correcciones…",
"mergingPr": "Fusionando PR…",
"startPrReview": "Iniciar revisión del PR",
"statusRefreshed": "Estado del PR actualizado"
"statusRefreshed": "Estado del PR actualizado",
"label": ""
},
"priority": {
"ariaLabel": "Prioridad de la tarea",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "Creado por",
"createdVia": "Creado mediante"
"createdVia": "Creado mediante",
"parentTaskOf": ""
},
"recoveryState": "Estado de recuperación",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Cambiar a Markdown",
"switchToPlain": "Cambiar a texto sin formato",
"thinkingLevel": "",
"workflowName": ""
"workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
},
"workflowColumns": {
"add": "",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "",
"editingPrompt": "",
"expandPrompt": ""
"expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
},
"workflowFields": {
"add": "Agregar campo",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Pasos de plugin",
"templatesSection": "Plantillas",
"timeoutMs": "",
"trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo."
"trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
},
"workflows": {
"aiEdit": "Diseñar con IA",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nodos",
"templatePickerLabel": "Comenzar desde",
"templateSectionBuiltin": "Flujos de trabajo integrados",
"templateSectionYours": "Tus flujos de trabajo"
"templateSectionYours": "Tus flujos de trabajo",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
},
"workflowSelector": {
"switchActiveMessage": "",
"switchActiveTitle": "",
"switchCancel": "",
"switchConfirm": ""
"switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
},
"workflowSettings": {
"add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Solicitud de instalación de Worktrunk",
"sha256": "SHA-256",
"version": "Versión"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": ""
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "",
"online": ""
},
"labels": {
"name": ""
},
"merge": {
"unknown": ""
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": ""
},
"todo": "À faire",
"triage": "Triage"
"triage": "Triage",
"workflow": {
"edit": "",
"new": ""
}
},
"branchGroup": {
"abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "Commentaires",
"placeholder": "Ajouter un commentaire",
"postingButton": "Publication en cours…",
"updatedSuccess": "Commentaire mis à jour"
"updatedSuccess": "Commentaire mis à jour",
"aiGuidance": ""
},
"commit": {
"filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "Relancer les vérifications de pré-vol",
"revertToAi": "Revenir à la version IA",
"titleLabel": "Titre",
"usingTemplate": "Utilisation de <code>.github/pull_request_template.md</code>"
"usingTemplate": "Utilisation de <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
},
"preview": {
"blockedDescription": "Vous pouvez afficher l'aperçu dans un onglet du navigateur séparé.",
@@ -5553,7 +5611,8 @@
"totalSize": "",
"view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
"whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
},
"clearToDefault": "",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "Aide",
"version": "Version {{version}}"
"version": "Version {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
},
"general": {
"25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
},
"importExport": {
"confirmImport": "Confirmer l'importation",
@@ -5756,7 +5820,35 @@
"importing": "Importation…",
"importTitle": "Importer les paramètres",
"loadingFile": "Chargement…",
"reviewPrompt": "Vérifiez les paramètres à importer :"
"reviewPrompt": "Vérifiez les paramètres à importer :",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
},
"jsonPlaceholder": "Entrez une valeur JSON...",
"keepLocal": "Garder la version locale",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "",
"testing": "Test en cours…",
"testRetrieval": "Tester la récupération",
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
"turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
},
"merge": {
"abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "Utiliser l'existant",
"usingQuickTunnel": ""
"usingQuickTunnel": "",
"installationFailed": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": ""
"whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
},
"scope": {
"globalBanner": "Ces paramètres sont partagés entre tous vos projets Fusion.",
@@ -6335,7 +6433,12 @@
"worktrunk": "",
"worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "",
"worktrunkIntegration": ""
"worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "Créer des tâches manuellement",
"withoutGitHub2": "Décrire le travail pour les agents IA",
"withoutGitHub3": "Suivre la progression sur le tableau",
"withoutGitHubHeading": "Sans GitHub (disponible maintenant) :"
"withoutGitHubHeading": "Sans GitHub (disponible maintenant) :",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
},
"shell": {
"activePill": "Actif",
@@ -7201,7 +7307,8 @@
"mergingFixes": "Fusion des corrections…",
"mergingPr": "Fusion du PR…",
"startPrReview": "Démarrer la révision du PR",
"statusRefreshed": "Statut du PR actualisé"
"statusRefreshed": "Statut du PR actualisé",
"label": ""
},
"priority": {
"ariaLabel": "Priorité de la tâche",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "Créé par",
"createdVia": "Créé via"
"createdVia": "Créé via",
"parentTaskOf": ""
},
"recoveryState": "État de récupération",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Basculer vers Markdown",
"switchToPlain": "Basculer vers texte brut",
"thinkingLevel": "",
"workflowName": ""
"workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
},
"workflowColumns": {
"add": "",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "",
"editingPrompt": "",
"expandPrompt": ""
"expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
},
"workflowFields": {
"add": "Ajouter un champ",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "Étapes de plugin",
"templatesSection": "Modèles",
"timeoutMs": "",
"trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer."
"trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
},
"workflows": {
"aiEdit": "Concevoir avec l'IA",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} nœuds",
"templatePickerLabel": "Partir de",
"templateSectionBuiltin": "Workflows intégrés",
"templateSectionYours": "Vos workflows"
"templateSectionYours": "Vos workflows",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
},
"workflowSelector": {
"switchActiveMessage": "",
"switchActiveTitle": "",
"switchCancel": "",
"switchConfirm": ""
"switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
},
"workflowSettings": {
"add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Demande d'installation de Worktrunk",
"sha256": "SHA-256",
"version": "Version"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": ""
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "",
"online": ""
},
"labels": {
"name": ""
},
"merge": {
"unknown": ""
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": ""
},
"todo": "할 일",
"triage": "트리아지"
"triage": "트리아지",
"workflow": {
"edit": "",
"new": ""
}
},
"branchGroup": {
"abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "댓글",
"placeholder": "댓글 추가",
"postingButton": "게시 중…",
"updatedSuccess": "댓글이 업데이트되었습니다"
"updatedSuccess": "댓글이 업데이트되었습니다",
"aiGuidance": ""
},
"commit": {
"filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "사전 확인 다시 실행",
"revertToAi": "AI 버전으로 되돌리기",
"titleLabel": "제목",
"usingTemplate": "<code>.github/pull_request_template.md</code> 템플릿 사용 중"
"usingTemplate": "<code>.github/pull_request_template.md</code> 템플릿 사용 중",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
},
"preview": {
"blockedDescription": "별도의 브라우저 탭에서 미리보기를 볼 수 있습니다.",
@@ -5553,7 +5611,8 @@
"totalSize": "",
"view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
"whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
},
"clearToDefault": "",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "도움말",
"version": "버전 {{version}}"
"version": "버전 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
},
"general": {
"25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
},
"importExport": {
"confirmImport": "가져오기 확인",
@@ -5756,7 +5820,35 @@
"importing": "가져오는 중…",
"importTitle": "설정 가져오기",
"loadingFile": "불러오는 중…",
"reviewPrompt": "가져올 설정을 검토하세요:"
"reviewPrompt": "가져올 설정을 검토하세요:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
},
"jsonPlaceholder": "JSON 값을 입력하세요...",
"keepLocal": "로컬 유지",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "",
"testing": "테스트 중…",
"testRetrieval": "검색 테스트",
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
"turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
},
"merge": {
"abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "기존 사용",
"usingQuickTunnel": ""
"usingQuickTunnel": "",
"installationFailed": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": ""
"whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
},
"scope": {
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
@@ -6335,7 +6433,12 @@
"worktrunk": "",
"worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "",
"worktrunkIntegration": ""
"worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "작업 수동 생성",
"withoutGitHub2": "AI 에이전트를 위한 작업 설명",
"withoutGitHub3": "보드에서 진행 상황 추적",
"withoutGitHubHeading": "GitHub 없이 (지금 사용 가능):"
"withoutGitHubHeading": "GitHub 없이 (지금 사용 가능):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
},
"shell": {
"activePill": "활성",
@@ -7201,7 +7307,8 @@
"mergingFixes": "수정 사항 병합 중…",
"mergingPr": "PR 병합 중…",
"startPrReview": "PR 검토 시작",
"statusRefreshed": "PR 상태가 새로 고침되었습니다"
"statusRefreshed": "PR 상태가 새로 고침되었습니다",
"label": ""
},
"priority": {
"ariaLabel": "작업 우선순위",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "작성자",
"createdVia": "생성 경로"
"createdVia": "생성 경로",
"parentTaskOf": ""
},
"recoveryState": "복구 상태",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "Markdown으로 전환",
"switchToPlain": "일반 텍스트로 전환",
"thinkingLevel": "",
"workflowName": ""
"workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
},
"workflowColumns": {
"add": "",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "",
"editingPrompt": "",
"expandPrompt": ""
"expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
},
"workflowFields": {
"add": "필드 추가",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "플러그인 단계",
"templatesSection": "템플릿",
"timeoutMs": "",
"trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요."
"trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요.",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
},
"workflows": {
"aiEdit": "AI로 디자인",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "노드 {{count}}개",
"templatePickerLabel": "시작점",
"templateSectionBuiltin": "기본 제공 워크플로",
"templateSectionYours": "내 워크플로"
"templateSectionYours": "내 워크플로",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
},
"workflowSelector": {
"switchActiveMessage": "",
"switchActiveTitle": "",
"switchCancel": "",
"switchConfirm": ""
"switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
},
"workflowSettings": {
"add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 설치 요청",
"sha256": "SHA-256",
"version": "버전"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": ""
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "",
"online": ""
},
"labels": {
"name": ""
},
"merge": {
"unknown": ""
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": ""
},
"todo": "待办",
"triage": "分诊"
"triage": "分诊",
"workflow": {
"edit": "",
"new": ""
}
},
"branchGroup": {
"abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "评论",
"placeholder": "添加评论",
"postingButton": "发布中…",
"updatedSuccess": "评论已更新"
"updatedSuccess": "评论已更新",
"aiGuidance": ""
},
"commit": {
"filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "重新运行飞行前检查",
"revertToAi": "恢复为 AI 版本",
"titleLabel": "标题",
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>"
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
},
"preview": {
"blockedDescription": "您可以在单独的浏览器标签页中查看预览。",
@@ -5553,7 +5611,8 @@
"totalSize": "",
"view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
"whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
},
"clearToDefault": "",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "帮助",
"version": "版本 {{version}}"
"version": "版本 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
},
"general": {
"25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
},
"importExport": {
"confirmImport": "确认导入",
@@ -5756,7 +5820,35 @@
"importing": "导入中…",
"importTitle": "导入设置",
"loadingFile": "加载中…",
"reviewPrompt": "查看要导入的设置:"
"reviewPrompt": "查看要导入的设置:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
},
"jsonPlaceholder": "输入 JSON 值...",
"keepLocal": "保留本地",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "",
"testing": "测试中…",
"testRetrieval": "测试检索",
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
"turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
},
"merge": {
"abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "使用现有",
"usingQuickTunnel": ""
"usingQuickTunnel": "",
"installationFailed": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": ""
"whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
},
"scope": {
"globalBanner": "这些设置在所有 Fusion 项目中共享。",
@@ -6335,7 +6433,12 @@
"worktrunk": "",
"worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "",
"worktrunkIntegration": ""
"worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "手动创建任务",
"withoutGitHub2": "为 AI 代理描述工作",
"withoutGitHub3": "在看板上跟踪进度",
"withoutGitHubHeading": "不使用 GitHub(现在可用):"
"withoutGitHubHeading": "不使用 GitHub(现在可用):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
},
"shell": {
"activePill": "活跃",
@@ -7201,7 +7307,8 @@
"mergingFixes": "正在合并修复…",
"mergingPr": "正在合并 PR…",
"startPrReview": "开始 PR 审查",
"statusRefreshed": "PR 状态已刷新"
"statusRefreshed": "PR 状态已刷新",
"label": ""
},
"priority": {
"ariaLabel": "任务优先级",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "创建者:",
"createdVia": "通过…创建"
"createdVia": "通过…创建",
"parentTaskOf": ""
},
"recoveryState": "恢复状态",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "切换到 Markdown",
"switchToPlain": "切换到纯文本",
"thinkingLevel": "",
"workflowName": ""
"workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
},
"workflowColumns": {
"add": "",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "",
"editingPrompt": "",
"expandPrompt": ""
"expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
},
"workflowFields": {
"add": "添加字段",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "插件步骤",
"templatesSection": "模板",
"timeoutMs": "",
"trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。"
"trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
},
"workflows": {
"aiEdit": "用 AI 设计",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} 个节点",
"templatePickerLabel": "从…开始",
"templateSectionBuiltin": "内置工作流",
"templateSectionYours": "我的工作流"
"templateSectionYours": "我的工作流",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
},
"workflowSelector": {
"switchActiveMessage": "",
"switchActiveTitle": "",
"switchCancel": "",
"switchConfirm": ""
"switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
},
"workflowSettings": {
"add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 安装请求",
"sha256": "SHA-256",
"version": "版本"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": ""
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "",
"online": ""
},
"labels": {
"name": ""
},
"merge": {
"unknown": ""
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",

View File

@@ -1181,7 +1181,11 @@
"workflowMismatch": ""
},
"todo": "待辦",
"triage": "分診"
"triage": "分診",
"workflow": {
"edit": "",
"new": ""
}
},
"branchGroup": {
"abandonGroup": "",
@@ -1810,7 +1814,8 @@
"heading": "評論",
"placeholder": "新增評論",
"postingButton": "發佈中…",
"updatedSuccess": "評論已更新"
"updatedSuccess": "評論已更新",
"aiGuidance": ""
},
"commit": {
"filesChanged_one": "",
@@ -4686,7 +4691,60 @@
"rerunPreflight": "重新執行飛行前檢查",
"revertToAi": "恢復為 AI 版本",
"titleLabel": "標題",
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>"
"usingTemplate": "使用 <code>.github/pull_request_template.md</code>",
"assignees": "",
"dismissConflictResolutionError": "",
"dismissPushBranchError": "",
"error": {
"actionOpen": "",
"actionRun": "",
"docs": ""
},
"generatingBody": "",
"generatingTitle": "",
"labels": "",
"loadingOptions": "",
"loadingPreflight": "",
"noChangedFiles": "",
"noCommits": "",
"pushBranch": {
"button": "",
"message": "",
"title": ""
},
"resolveConflicts": {
"button": "",
"message": "",
"title": ""
},
"reviewers": "",
"view": {
"agentDisagreed": "",
"agentReplyFix": "",
"approve": "",
"autoMerge": "",
"close": "",
"confirmMerge": "",
"creating": "",
"creationFailed": "",
"loading": "",
"merge": "",
"mergeableLabel": "",
"mergeDisabledUntilVerified": "",
"none": "",
"noReviewThreads": "",
"requestRetry": "",
"resolveConflictsBeforeMerge": "",
"resolveConflictsOnGithub": "",
"responseAlreadyInProgress": "",
"responsePending_one": "",
"responsePending_other": "",
"retryCreation": "",
"reviewLabel": "",
"threadFixed": "",
"threadPending": "",
"verifyingGithub": ""
}
},
"preview": {
"blockedDescription": "您可以在單獨的瀏覽器分頁中查看預覽。",
@@ -5553,7 +5611,8 @@
"totalSize": "",
"view": "",
"whenEnabledProjectAndAgentMemoryFilesAre": "",
"whenEnabledTheDatabaseIsBackedUpAutomatically": ""
"whenEnabledTheDatabaseIsBackedUpAutomatically": "",
"createFailed": ""
},
"clearToDefault": "",
"cliAgents": {
@@ -5597,7 +5656,9 @@
},
"footer": {
"help": "說明",
"version": "版本 {{version}}"
"version": "版本 {{version}}",
"checkUpdates": "",
"helpDiscussions": ""
},
"general": {
"25": "",
@@ -5746,7 +5807,10 @@
"whenEnabledStartupRefreshesModelsThroughTheLocal": ""
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "",
"star": "",
"starFusion": ""
},
"importExport": {
"confirmImport": "確認匯入",
@@ -5756,7 +5820,35 @@
"importing": "匯入中…",
"importTitle": "匯入設定",
"loadingFile": "載入中…",
"reviewPrompt": "查看要匯入的設定:"
"reviewPrompt": "查看要匯入的設定:",
"counts": {
"global_one": "",
"global_other": "",
"project_one": "",
"project_other": "",
"workflowSettings_one": "",
"workflowSettings_other": ""
},
"exported": "",
"exportFailed": "",
"globalSettings": "",
"imported": "",
"importFailed": "",
"importFailedDetailed": "",
"importScope": "",
"importTitleAttr": "",
"invalidJson": "",
"mergeExisting": "",
"projectSettings": "",
"replaceWarning": "",
"scopeBoth": "",
"scopeGlobal": "",
"scopeLabel": {
"all": "",
"global": "",
"project": ""
},
"scopeProject": ""
},
"jsonPlaceholder": "輸入 JSON 值...",
"keepLocal": "保留本機",
@@ -5806,7 +5898,10 @@
"searchMemoryWithQmd": "",
"testing": "測試中…",
"testRetrieval": "測試擷取",
"turnsDailyNotesIntoDREAMSMdAndPromotes": ""
"turnsDailyNotesIntoDREAMSMdAndPromotes": "",
"qmdInstalled": "",
"qmdInstallFailed": "",
"qmdInstallUnavailable": ""
},
"merge": {
"abort": "",
@@ -6155,7 +6250,8 @@
"uRLAndQRGenerationUseTheSelectedToken": "",
"uRLNoHostnameOrPortConfigurationNeeded": "",
"useExisting": "使用現有",
"usingQuickTunnel": ""
"usingQuickTunnel": "",
"installationFailed": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",
@@ -6275,7 +6371,9 @@
"timeoutInMinutesForDetectingStuckTasksWhen": "",
"whenEnabledTasksThatModifyTheSameFiles": "",
"whenEnabledTasksWithStalePlansPROMPTMd": "",
"whenTheStuckDetectorKillsAndReQueues": ""
"whenTheStuckDetectorKillsAndReQueues": "",
"browseWorkspacePath": "",
"overlapPickerNote": ""
},
"scope": {
"globalBanner": "這些設定在所有 Fusion 專案中共用。",
@@ -6335,7 +6433,12 @@
"worktrunk": "",
"worktrunkBinaryPath": "",
"worktrunkFailureBehavior": "",
"worktrunkIntegration": ""
"worktrunkIntegration": "",
"worktreesPickerNote": ""
},
"fileBrowser": {
"currentDirectory": "",
"projectRoot": ""
}
},
"setup": {
@@ -6655,7 +6758,10 @@
"withoutGitHub1": "手動建立任務",
"withoutGitHub2": "為 AI 代理描述工作",
"withoutGitHub3": "在看板上追蹤進度",
"withoutGitHubHeading": "不使用 GitHub(現在可用):"
"withoutGitHubHeading": "不使用 GitHub(現在可用):",
"brandLogo": "",
"brandName": "",
"setupCompleteTitle": ""
},
"shell": {
"activePill": "作用中",
@@ -7201,7 +7307,8 @@
"mergingFixes": "正在合併修復…",
"mergingPr": "正在合併 PR…",
"startPrReview": "開始 PR 審查",
"statusRefreshed": "PR 狀態已刷新"
"statusRefreshed": "PR 狀態已刷新",
"label": ""
},
"priority": {
"ariaLabel": "任務優先級",
@@ -7216,7 +7323,8 @@
},
"provenance": {
"createdBy": "創建者:",
"createdVia": "通過…建立"
"createdVia": "通過…建立",
"parentTaskOf": ""
},
"recoveryState": "恢復狀態",
"refine": {
@@ -7955,7 +8063,43 @@
"switchToMarkdown": "切換為 Markdown",
"switchToPlain": "切換為純文字",
"thinkingLevel": "",
"workflowName": ""
"workflowName": "",
"aggregateAdvisory": "",
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"approveAndRun": "",
"approveCommandError": "",
"approving": "",
"awaitingInputTitle": "",
"cliApprovalRejectHint": "",
"cliApprovalTitle": "",
"cliApprovalWarning": "",
"customWorkflowFallback": "",
"customWorkflowLabel": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"inputPlaceholder": "",
"modelDefault": "",
"postMerge": "",
"preMerge": "",
"replyInComments": "",
"resumeTaskError": "",
"resuming": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"submitAndResume": "",
"submitting": "",
"waitingForOutput": ""
},
"workflowColumns": {
"add": "",
@@ -8007,7 +8151,30 @@
},
"collapsePrompt": "",
"editingPrompt": "",
"expandPrompt": ""
"expandPrompt": "",
"agent": "",
"agentsLoadFailed": "",
"autoApproveRequests": "",
"autoApproveRequestsNote": "",
"cliCommandNote": "",
"cliMode": "",
"cliScript": "",
"command": "",
"executor": "",
"maxRetries": "",
"model": "",
"modelsLoadFailed": "",
"namedScript": "",
"namedScriptNote": "",
"prompt": "",
"scriptName": "",
"selectAgent": "",
"selectSkill": "",
"skill": "",
"skillsLoadFailed": "",
"skipFirstRunApproval": "",
"waitForUserInput": "",
"waitForUserInputNote": ""
},
"workflowFields": {
"add": "新增欄位",
@@ -8141,7 +8308,11 @@
"templatesPluginSteps": "外掛步驟",
"templatesSection": "範本",
"timeoutMs": "",
"trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。"
"trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。",
"conditionFailure": "",
"conditionSuccess": "",
"nodeInspector": "",
"readOnlyDuplicateToEdit": ""
},
"workflows": {
"aiEdit": "使用 AI 設計",
@@ -8204,13 +8375,27 @@
"templateNodeCount_other": "{{count}} 個節點",
"templatePickerLabel": "起始來源",
"templateSectionBuiltin": "內建工作流程",
"templateSectionYours": "你的工作流程"
"templateSectionYours": "你的工作流程",
"closeEditor": "",
"duplicatedEditable": "",
"duplicateFailed": "",
"loadFailed": "",
"loading": "",
"noneYet": "",
"title": ""
},
"workflowSelector": {
"switchActiveMessage": "",
"switchActiveTitle": "",
"switchCancel": "",
"switchConfirm": ""
"switchConfirm": "",
"applyFailed": "",
"defaultCleared": "",
"defaultSet": "",
"defaultWorkflowLabel": "",
"loadFailed": "",
"manage": "",
"none": ""
},
"workflowSettings": {
"add": "",
@@ -8288,5 +8473,50 @@
"installRequestTitle": "Worktrunk 安裝請求",
"sha256": "SHA-256",
"version": "版本"
},
"taskChat": {
"activePlaceholder": "",
"activeSessionHint": "",
"agentMessages": "",
"arguments": "",
"collapseChat": "",
"donePlaceholder": "",
"doneSessionHint": "",
"emptyAgentOutput": "",
"entryCount_one": "",
"entryCount_other": "",
"error": "",
"errorCount_one": "",
"errorCount_other": "",
"expandChat": "",
"idleSessionHint": "",
"jumpToLatestMessage": "",
"latest": "",
"loadingAgentOutput": "",
"loadingEarlierMessages": "",
"loadPreviousMessages": "",
"message": "",
"messageActiveAgentSession": "",
"moreTools_one": "",
"moreTools_other": "",
"result": "",
"roles": {
"agent": "",
"executor": "",
"merger": "",
"planner": "",
"reviewer": ""
},
"sending": "",
"thinking": "",
"toolCall": "",
"toolCallCount_one": "",
"toolCallCount_other": "",
"toolCallTo": "",
"toolError": "",
"toolNames": "",
"toolResult": "",
"you": "",
"youMessage": ""
}
}

View File

@@ -1,4 +1,7 @@
{
"actions": {
"send": ""
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -78,6 +81,9 @@
"offline": "",
"online": ""
},
"labels": {
"name": ""
},
"merge": {
"unknown": ""
},
@@ -214,28 +220,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"workflow": {
"aggregateAllPassed": "",
"aggregateInProgress": "",
"aggregateNoResults": "",
"customWorkflowFallback": "",
"defaultWorkflow": "",
"executionAwaitingCliApproval": "",
"executionAwaitingInput": "",
"executionCompleted": "",
"executionNotStarted": "",
"executionPaused": "",
"executionPostMerge": "",
"executionPreMerge": "",
"postMerge": "",
"preMerge": "",
"statusAdvisory": "",
"statusFailed": "",
"statusPassed": "",
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",

View File

@@ -1183,7 +1183,11 @@ export default interface Resources {
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
},
"todo": "To Do",
"triage": "Triage"
"triage": "Triage",
"workflow": {
"edit": "Edit workflows",
"new": "New workflow"
}
},
"branchGroup": {
"abandonGroup": "Abandon group",
@@ -1805,6 +1809,7 @@ export default interface Resources {
"comments": {
"addButton": "Add Comment",
"addedSuccess": "Comment added",
"aiGuidance": "AI Guidance",
"deletedSuccess": "Comment deleted",
"deletingButton": "Deleting…",
"editedSuffix": "(edited)",
@@ -4661,6 +4666,7 @@ export default interface Resources {
"viewUnavailable": "Plugin view unavailable"
},
"pr": {
"assignees": "Assignees",
"authFail": "Run gh auth login and try again.",
"authOk": "GitHub CLI auth is available.",
"baseBranch": "Base branch",
@@ -4680,15 +4686,67 @@ export default interface Resources {
"createDraftPr": "Create draft PR",
"createPr": "Create PR",
"createTitle": "Create Pull Request",
"dismissConflictResolutionError": "Dismiss conflict resolution error",
"dismissError": "Dismiss PR error",
"dismissPushBranchError": "Dismiss push branch error",
"error": {
"actionOpen": "Action: open",
"actionRun": "Action: run",
"docs": "docs"
},
"generatingBody": "Generating AI body…",
"generatingTitle": "Generating AI title…",
"labels": "Labels",
"loadingOptions": "Loading PR options…",
"loadingPreflight": "Loading pre-flight checks…",
"noChangedFiles": "No changed files detected.",
"noCommits": "No commits found.",
"noConflicts": "No merge conflicts detected.",
"preflightChecks": "Pre-flight checks",
"previewTitle": "Diff & commit preview",
"pushBranch": {
"button": "Push branch to remote",
"message": "Fusion will push this task's branch to origin so the PR can be created.",
"title": "Push branch to remote"
},
"regenerate": "Regenerate",
"rerunPreflight": "Re-run preflight",
"resolveConflicts": {
"button": "Resolve conflicts with AI",
"message": "Fusion will use AI to resolve conflicts on this branch and push it.",
"title": "Resolve conflicts with AI"
},
"revertToAi": "Revert to AI version",
"reviewers": "Reviewers",
"titleLabel": "Title",
"usingTemplate": "Using <code>.github/pull_request_template.md</code>"
"usingTemplate": "Using <code>.github/pull_request_template.md</code>",
"view": {
"agentDisagreed": "agent disagreed",
"agentReplyFix": "Agent reply — fix {{sha}}",
"approve": "Approve",
"autoMerge": "Auto-merge",
"close": "Close",
"confirmMerge": "Confirm merge",
"creating": "Creating PR…",
"creationFailed": "PR creation failed",
"loading": "Loading PR…",
"merge": "Merge",
"mergeDisabledUntilVerified": "Merge is disabled until GitHub verifies this PR",
"mergeableLabel": "Mergeable:",
"noReviewThreads": "No review threads.",
"none": "none",
"requestRetry": "Request retry",
"resolveConflictsBeforeMerge": "Resolve conflicts on GitHub before merging",
"resolveConflictsOnGithub": "Resolve conflicts on GitHub",
"responseAlreadyInProgress": "A response run is already in progress",
"responsePending_one": "Response run in progress — {{count}} threads pending",
"responsePending_other": "Response run in progress — {{count}} threads pending",
"retryCreation": "Retry PR creation",
"reviewLabel": "Review:",
"threadFixed": "fixed",
"threadPending": "pending",
"verifyingGithub": "Verifying with GitHub…"
}
},
"preview": {
"blockedDescription": "You can view the preview in a separate browser tab.",
@@ -5527,6 +5585,7 @@ export default interface Resources {
"backupS": " backup(s)",
"backupScheduleCron": "Backup Schedule (Cron)",
"backups": "backups",
"createFailed": "Failed to create backup",
"creating": "Creating…",
"cronExpressionForBackupTimingDefault02": " Cron expression for backup timing. Default: 0 2 * * * (daily at 2 AM). Examples: 0 * * * * (hourly), 0 0 * * 0 (weekly), */15 * * * * (every 15 min) ",
"cronExpressionForMemoryBackupTimingDefault0": "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).",
@@ -5597,8 +5656,14 @@ export default interface Resources {
"experimentalFeaturesAreEarlyCapabilitiesThatAreNot": " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. ",
"featureFlags": "Feature Flags"
},
"fileBrowser": {
"currentDirectory": "Current directory:",
"projectRoot": "(project root)"
},
"footer": {
"checkUpdates": "Check for updates",
"help": "Help",
"helpDiscussions": "Help and discussions",
"version": "Version {{version}}"
},
"general": {
@@ -5748,17 +5813,48 @@ export default interface Resources {
"whenEnabledStartupRefreshesModelsThroughTheLocal": " When enabled, startup refreshes models through the local "
},
"header": {
"discord": "Discord"
"discord": "Discord",
"joinDiscord": "Join our Discord",
"star": "Star",
"starFusion": "Star Fusion on GitHub"
},
"importExport": {
"confirmImport": "Confirm Import",
"counts": {
"global_one": "{{count}} global",
"global_other": "{{count}} global",
"project_one": "{{count}} project",
"project_other": "{{count}} project",
"workflowSettings_one": "{{count}} workflow setting value",
"workflowSettings_other": "{{count}} workflow setting value"
},
"exportBtn": "Export",
"exportFailed": "Failed to export settings",
"exportTitle": "Export settings to JSON file",
"exported": "Settings exported ({{scope}} scope)",
"globalSettings": "Global Settings:",
"importBtn": "Import",
"importFailed": "Import failed",
"importFailedDetailed": "Failed to import settings",
"importScope": "Import Scope:",
"importTitle": "Import Settings",
"importTitleAttr": "Import settings from JSON file",
"imported": "Imported {{counts}} setting(s)",
"importing": "Importing…",
"invalidJson": "Invalid JSON file: {{error}}",
"loadingFile": "Loading…",
"reviewPrompt": "Review the settings to be imported:"
"mergeExisting": "Merge with existing settings (recommended)",
"projectSettings": "Project Settings:",
"replaceWarning": "If unchecked, existing settings will be replaced with imported values.",
"reviewPrompt": "Review the settings to be imported:",
"scopeBoth": "Both global and project settings",
"scopeGlobal": "Global settings only",
"scopeLabel": {
"all": "all",
"global": "global",
"project": "project"
},
"scopeProject": "Project settings only"
},
"jsonPlaceholder": "Enter JSON value...",
"keepLocal": "Keep Local",
@@ -5800,6 +5896,9 @@ export default interface Resources {
"noMatchingMemoryFound": "No matching memory found.",
"processDreamsFromDailyMemory": " Process dreams from daily memory ",
"qmd": " qmd ",
"qmdInstallFailed": "Failed to install qmd",
"qmdInstallUnavailable": "qmd install finished, but qmd is still unavailable",
"qmdInstalled": "qmd installed successfully",
"qmdIsNotInstalledSearchWillUseLocal": " qmd is not installed. Search will use local files. Install indexed retrieval: ",
"result": " result",
"runsTheSameQmdBackedMemorySearchPath": "Runs the same qmd-backed memory_search path agents use.",
@@ -6115,6 +6214,7 @@ export default interface Resources {
"ifHomebrewIsUnavailable": "If Homebrew is unavailable: ",
"ingressURL": "Ingress URL",
"installCloudflared": "Install cloudflared",
"installationFailed": "Installation failed",
"installing": "Installing…",
"lastShortLivedTokenExpiresAt": "Last short-lived token expires at ",
"manualInstall": "Manual install: ",
@@ -6235,6 +6335,7 @@ export default interface Resources {
"archiveCompletedTasksAfterDays": "Archive Completed Tasks After (days)",
"backlogNoTaskAutoClaimIsExecutorOnly": "Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.",
"browse": " Browse ",
"browseWorkspacePath": "Browse workspace path",
"closeParenPeriod": ").",
"compactModeKeepsArchiveSizeLowWhilePreserving": "Compact mode keeps archive size low while preserving recent agent activity for context.",
"compactSummaryAndRecentEntries": "Compact summary and recent entries",
@@ -6261,6 +6362,7 @@ export default interface Resources {
"off": "Off",
"optionalFileOrDirectoryPathsToIgnoreWhen": " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ",
"or": " or ",
"overlapPickerNote": "Choose a file to ignore directly, or navigate into a folder and select the current directory.",
"pollIntervalMs": "Poll Interval (ms)",
"preserveStepProgressOnStuckTaskRequeue": " Preserve step progress on stuck-task requeue ",
"remove": " Remove ",
@@ -6334,6 +6436,7 @@ export default interface Resources {
"worktreeNamingStyle": "Worktree Naming Style",
"worktrees": "Worktrees",
"worktreesDirectory": "Worktrees Directory",
"worktreesPickerNote": "Navigate to the folder where Fusion should create task worktrees, then select the current directory.",
"worktrunk": " worktrunk ",
"worktrunkBinaryPath": "Worktrunk binary path",
"worktrunkFailureBehavior": "Worktrunk failure behavior",
@@ -6362,6 +6465,8 @@ export default interface Resources {
"authToken": "Auth Token",
"authTokenOptional": "Auth token (optional)",
"back": "← Back",
"brandLogo": "Fusion logo",
"brandName": "Fusion",
"browserAuthToken": "Browser Auth Token",
"cancelLogin": "Cancel",
"childProcess": "Child-Process",
@@ -6602,6 +6707,7 @@ export default interface Resources {
"setUpAi": "Set Up AI",
"setUpProject": "Set Up Project",
"setupComplete": "Setup complete! Head to the board to create your first task, or explore the dashboard to see what's available.",
"setupCompleteTitle": "Setup Complete!",
"setupMode": "Setup Mode",
"setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.",
"skip": "Skip",
@@ -6925,6 +7031,51 @@ export default interface Resources {
"toggleWordWrap": "Toggle word wrap",
"unavailable": "Detailed file changes unavailable."
},
"taskChat": {
"activePlaceholder": "Steer the currently executing agent",
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
"agentMessages": "{{label}} messages",
"arguments": "Arguments",
"collapseChat": "Collapse chat",
"donePlaceholder": "Start a refinement task for this completed task",
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
"entryCount_one": "{{count}} entry",
"entryCount_other": "{{count}} entries",
"error": "Error",
"errorCount_one": "{{count}} error",
"errorCount_other": "{{count}} errors",
"expandChat": "Expand chat to full modal",
"idleSessionHint": "No agent is working on this task right now. Your message is saved as guidance and will reach an agent the next time this task runs.",
"jumpToLatestMessage": "Jump to latest message",
"latest": "Latest",
"loadPreviousMessages": "Load previous messages",
"loadingAgentOutput": "Loading agent output…",
"loadingEarlierMessages": "Loading earlier messages…",
"message": "Message",
"messageActiveAgentSession": "Message active agent session",
"moreTools_one": ", +{{count}} more",
"moreTools_other": ", +{{count}} more",
"result": "Result",
"roles": {
"agent": "Agent",
"executor": "Executor",
"merger": "Merger",
"planner": "Planner",
"reviewer": "Reviewer"
},
"sending": "Sending",
"thinking": "Thinking",
"toolCall": "Tool call",
"toolCallCount_one": "{{count}} tool call",
"toolCallCount_other": "{{count}} tool calls",
"toolCallTo": "Tool call → {{label}}",
"toolError": "Tool error",
"toolNames": "Tool names",
"toolResult": "Tool result",
"you": "You",
"youMessage": "You message"
},
"taskDetail": {
"actions": {
"menuBtn": "Actions"
@@ -7199,6 +7350,7 @@ export default interface Resources {
"checkPrStatus": "Check PR Status",
"creatingPr": "Creating PR…",
"finishAndClose": "Finish & Close",
"label": "PR",
"mergeAndClose": "Merge & Close",
"mergingFixes": "Merging fixes…",
"mergingPr": "Merging PR…",
@@ -7218,7 +7370,8 @@ export default interface Resources {
},
"provenance": {
"createdBy": "Created by",
"createdVia": "Created via"
"createdVia": "Created via",
"parentTaskOf": "of"
},
"recoveryState": "Recovery state",
"refine": {
@@ -7903,22 +8056,45 @@ export default interface Resources {
},
"workflow": {
"advisoryExplanation": "Advisory workflow steps flagged non-blocking improvements:",
"aggregateAdvisory": "Advisory",
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"aggregateResult": "Aggregate result",
"approveAndRun": "Approve & run",
"approveCommandError": "Failed to approve command",
"approving": "Approving…",
"awaitingInputTitle": "Waiting for your input",
"cliApprovalRejectHint": "To reject, keep the task paused and do not approve.",
"cliApprovalTitle": "Approve CLI command?",
"cliApprovalWarning": "This command will run in the task worktree. Approving trusts this exact command for future runs.",
"configuredSteps": "Configured Workflow Steps",
"customWorkflowFallback": "Custom workflow",
"customWorkflowLabel": "Custom workflow",
"defaultWorkflow": "Default",
"done": "Done",
"doneEditingAriaLabel": "Done editing workflow steps",
"edit": "Edit",
"editAriaLabel": "Edit workflow steps",
"editWorkflow": "Edit workflow",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionOrder": "Execution order:",
"executionPaused": "Paused",
"executionPhase": "Execution phase",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"expandOutput": "Expand output",
"graph": "Workflow graph",
"graphUnavailable": "Workflow graph unavailable",
"hideOutput": "Hide output",
"inputPlaceholder": "Type your reply…",
"loadingGraph": "Loading workflow graph…",
"loadingResults": "Loading workflow results…",
"markdown": "Markdown",
"modelDefault": "Default",
"modelSettings": "Model settings",
"moveDown": "Move down",
"moveUp": "Move up",
@@ -7930,10 +8106,20 @@ export default interface Resources {
"overview": "Workflow overview",
"plain": "Plain",
"polishNotes": "Polish notes",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"remove": "Remove",
"replyInComments": "Reply in the comments and unpause the task to continue.",
"resumeTaskError": "Failed to resume task",
"resuming": "Resuming…",
"selectStepsDescription": "Select steps to run after task implementation completes",
"showOutput": "Show output",
"started": "Started:",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"stepCount_one": "{{count}} step",
"stepCount_other": "{{count}} steps",
"stepDefinitionNotFound": "Step definition not found.",
@@ -7941,6 +8127,8 @@ export default interface Resources {
"stepProgressValue": "{{completed}} of {{total}} steps completed",
"steps": "Workflow Steps",
"stepsExplanation": "Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.",
"submitAndResume": "Submit & resume",
"submitting": "Submitting…",
"summaryAdvisory_one": "{{count}} advisory",
"summaryAdvisory_other": "{{count}} advisory",
"summaryFailed_one": "{{count}} failed",
@@ -7957,6 +8145,7 @@ export default interface Resources {
"switchToMarkdown": "Switch to markdown",
"switchToPlain": "Switch to plain text",
"thinkingLevel": "Thinking level",
"waitingForOutput": "Waiting for agent output…",
"workflowName": "Workflow"
},
"workflowColumns": {
@@ -7993,6 +8182,10 @@ export default interface Resources {
"unplacedCount_other": "{{count}} nodes not placed in a column"
},
"workflowEditor": {
"agent": "Agent",
"agentsLoadFailed": "Failed to load agents",
"autoApproveRequests": "Auto-approve requests",
"autoApproveRequestsNote": "Runs without pausing for approval — e.g. a CLI command executes on its first run without waiting for your sign-off.",
"cliAgent": {
"adapterLabel": "CLI adapter",
"adapterNote": "Drives a CLI coding agent in an engine-owned terminal for this step.",
@@ -8007,9 +8200,28 @@ export default interface Resources {
"notifyLabel": "Waiting-on-input notification",
"notifyNote": "How you are alerted when the agent pauses waiting for input on this step."
},
"cliCommandNote": "Runs an arbitrary command in the task worktree. The first time this exact command runs, the task pauses for your approval. The node prompt is passed via FUSION_NODE_PROMPT.",
"cliMode": "CLI mode",
"cliScript": "CLI / script",
"collapsePrompt": "Collapse prompt editor",
"command": "Command",
"editingPrompt": "Editing Prompt",
"expandPrompt": "Expand prompt editor"
"executor": "Executor",
"expandPrompt": "Expand prompt editor",
"maxRetries": "Max retries",
"model": "Model",
"modelsLoadFailed": "Failed to load models",
"namedScript": "Named script",
"namedScriptNote": "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.",
"prompt": "Prompt",
"scriptName": "Script name",
"selectAgent": "— select agent —",
"selectSkill": "— select skill —",
"skill": "Skill",
"skillsLoadFailed": "Failed to load skills",
"skipFirstRunApproval": "Skip first-run approval (runs without pausing)",
"waitForUserInput": "Wait for user input",
"waitForUserInputNote": "This node pauses the task until you reply in the task's comments and unpause. The Prompt field above is shown to the user as the question."
},
"workflowFields": {
"add": "Add field",
@@ -8052,6 +8264,8 @@ export default interface Resources {
"codeSource": "Source (TypeScript)",
"codeTimeout": "Timeout (ms)",
"collapseInspector": "Collapse",
"conditionFailure": "failure",
"conditionSuccess": "success",
"cycleBlocked": "That connection would create a cycle — only rework edges inside a for-each template may loop back",
"deleteEdge": "Delete edge",
"deleteNode": "Delete node",
@@ -8108,6 +8322,7 @@ export default interface Resources {
"mobileMoveDown": "Move down",
"mobileMoveUp": "Move up",
"mobileNodeKinds": "Node types",
"nodeInspector": "Node",
"notifyCustom": "Custom",
"notifyCustomEvent": "Custom event",
"notifyEvent": "Event type",
@@ -8117,6 +8332,7 @@ export default interface Resources {
"parseArtifact": "Artifact",
"parseParser": "Parser",
"quorumN": "Quorum count (n)",
"readOnlyDuplicateToEdit": "Read-only built-in — duplicate the workflow to edit nodes.",
"releaseCapacity": "Downstream capacity",
"releaseCondition": "Release condition",
"releaseDependency": "Dependency complete",
@@ -8146,6 +8362,13 @@ export default interface Resources {
"trivialGraphHint": "This workflow only runs start → end. Add steps from the palette above to build it out."
},
"workflowSelector": {
"applyFailed": "Failed to apply workflow",
"defaultCleared": "Default workflow cleared",
"defaultSet": "Default workflow set",
"defaultWorkflowLabel": "Default workflow for new tasks",
"loadFailed": "Failed to load workflows",
"manage": "Manage…",
"none": "None",
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
"switchActiveTitle": "Switch workflow?",
"switchCancel": "Cancel",
@@ -8226,6 +8449,7 @@ export default interface Resources {
"backToWorkflowList": "Back to workflows",
"clickToEditDescription": "Click to edit description",
"clickToRename": "Click to rename",
"closeEditor": "Close workflow editor",
"createDescription": "Description (optional)",
"createFailed": "Failed to create workflow",
"createName": "Name",
@@ -8242,7 +8466,9 @@ export default interface Resources {
"discardConfirm": "Discard",
"discardMessage": "You have unsaved changes to this workflow. Discard them?",
"discardTitle": "Discard unsaved changes?",
"duplicateFailed": "Failed to duplicate workflow",
"duplicateToCustomize": "Duplicate to customize",
"duplicatedEditable": "Duplicated to \"{{name}}\" — editable",
"emptyDescription": "Workflows orchestrate the steps and gates that run around task execution. Create one to start arranging that flow.",
"emptyTitle": "No workflow selected",
"export": "Export",
@@ -8255,11 +8481,14 @@ export default interface Resources {
"importStripped": "Auto-approval flags were removed from imported nodes",
"importTooltip": "Import a workflow from a JSON file",
"imported": "Imported workflow \"{{name}}\"",
"loadFailed": "Failed to load workflows",
"loading": "Loading…",
"migrationNotice": "Your legacy workflow steps were converted — find them as templates in the palette and as the \"Migrated steps\" workflow.",
"mobileEditorNav": "Workflow editor sections",
"mobileSelectNote": "Select a workflow to edit.",
"nameLabel": "Workflow name",
"newWorkflow": "New workflow",
"noneYet": "No workflows yet.",
"readOnlyBuiltin": "Read-only built-in workflow",
"saveFailed": "Failed to save workflow",
"saved": "Workflow saved",
@@ -8273,7 +8502,8 @@ export default interface Resources {
"templateNodeCount_other": "{{count}} nodes",
"templatePickerLabel": "Start from",
"templateSectionBuiltin": "Built-in workflows",
"templateSectionYours": "Your workflows"
"templateSectionYours": "Your workflows",
"title": "Workflows"
},
"workspace": {
"projectRoot": "Project Root",
@@ -8519,6 +8749,9 @@ export default interface Resources {
}
},
"common": {
"actions": {
"send": "Send"
},
"agents": {
"ratings": {
"trendDeclining": "↓ Declining",
@@ -8598,6 +8831,9 @@ export default interface Resources {
"offline": "Offline",
"online": "Online"
},
"labels": {
"name": "Name"
},
"merge": {
"unknown": "Unknown"
},
@@ -8734,28 +8970,6 @@ export default interface Resources {
"refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual"
},
"workflow": {
"aggregateAllPassed": "All passed",
"aggregateInProgress": "In progress",
"aggregateNoResults": "No results",
"customWorkflowFallback": "Custom workflow",
"defaultWorkflow": "Default",
"executionAwaitingCliApproval": "Awaiting CLI approval",
"executionAwaitingInput": "Awaiting input",
"executionCompleted": "Completed",
"executionNotStarted": "Not started",
"executionPaused": "Paused",
"executionPostMerge": "Post-merge steps running",
"executionPreMerge": "Pre-merge steps running",
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
"statusAdvisory": "Advisory failure",
"statusFailed": "Failed",
"statusPassed": "Passed",
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": {
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",