fix(i18n): externalize remaining frontend copy (#2616)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-13 11:11:46 +01:00
committed by GitHub
parent df388c125d
commit ce82e2a4ab
92 changed files with 1542 additions and 356 deletions
+59 -36
View File
@@ -12,16 +12,17 @@ import {
type DiscoveredFile,
} from "@/lib/api/domains/debug-api";
import { IconChevronDown, IconChevronRight, IconRefresh } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
type ToolFilter = "all" | "tool_edit" | "tool_read" | "tool_execute" | "tool_call";
type ViewMode = "fixtures" | "events";
const TOOL_TABS: { value: ToolFilter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "tool_edit", label: "Edit" },
{ value: "tool_read", label: "Read" },
{ value: "tool_execute", label: "Execute" },
{ value: "tool_call", label: "Call" },
const TOOL_TABS: { value: ToolFilter; labelKey: string }[] = [
{ value: "all", labelKey: "common:all" },
{ value: "tool_edit", labelKey: "common:edit" },
{ value: "tool_read", labelKey: "common:read" },
{ value: "tool_execute", labelKey: "common:execute" },
{ value: "tool_call", labelKey: "common:call" },
];
function fixtureToMessage(fixture: NormalizedFixture, index: number): Message {
@@ -56,6 +57,7 @@ function fixtureToMessage(fixture: NormalizedFixture, index: number): Message {
// --- Hooks ---
function useAgentMessages() {
const { t } = useTranslation("common");
const [viewMode, setViewMode] = useState<ViewMode>("events");
const [fixtureFiles, setFixtureFiles] = useState<DiscoveredFile[]>([]);
const [selectedFixtureFile, setSelectedFixtureFile] = useState<string>("");
@@ -83,14 +85,13 @@ function useAgentMessages() {
setSelectedFixtureFile(fixtureFilesData[0].path);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load files");
setError(err instanceof Error ? err.message : t("common:demoFailedToLoadFiles"));
} finally {
setLoading(false);
}
}
loadFiles();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [selectedEventFile, selectedFixtureFile, t]);
const loadFixtures = useCallback(async () => {
if (!selectedFixtureFile) return;
@@ -100,11 +101,11 @@ function useAgentMessages() {
const data = await fetchNormalizedMessages(selectedFixtureFile);
setFixtures(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load fixtures");
setError(err instanceof Error ? err.message : t("common:demoFailedToLoadFixtures"));
} finally {
setLoading(false);
}
}, [selectedFixtureFile]);
}, [selectedFixtureFile, t]);
const loadMessages = useCallback(async () => {
if (!selectedEventFile) return;
@@ -114,11 +115,11 @@ function useAgentMessages() {
const data = await fetchNormalizedEventsAsMessages(selectedEventFile);
setMessages(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load messages");
setError(err instanceof Error ? err.message : t("common:demoFailedToLoadMessages"));
} finally {
setLoading(false);
}
}, [selectedEventFile]);
}, [selectedEventFile, t]);
useEffect(() => {
if (viewMode === "fixtures" && selectedFixtureFile) {
@@ -192,6 +193,7 @@ function JsonPanel({
}
function FixtureCard({ fixture, index }: { fixture: NormalizedFixture; index: number }) {
const { t } = useTranslation("common");
const message = fixtureToMessage(fixture, index);
return (
@@ -208,13 +210,15 @@ function FixtureCard({ fixture, index }: { fixture: NormalizedFixture; index: nu
</div>
</div>
<div className="p-4 border-b">
<div className="text-xs text-muted-foreground mb-2 font-medium">Rendered Output:</div>
<div className="text-xs text-muted-foreground mb-2 font-medium">
{t("common:demoRenderedOutput")}
</div>
<MessageRenderer comment={message} isTaskDescription={false} taskId="demo-task" />
</div>
<div className="p-4 space-y-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<JsonPanel title="Raw Input" data={fixture.input} />
<JsonPanel title="Normalized Payload" data={fixture.payload} />
<JsonPanel title={t("common:demoRawInput")} data={fixture.input} />
<JsonPanel title={t("common:demoNormalizedPayload")} data={fixture.payload} />
</div>
</div>
</div>
@@ -222,6 +226,7 @@ function FixtureCard({ fixture, index }: { fixture: NormalizedFixture; index: nu
}
function MessageCard({ message }: { message: Message }) {
const { t } = useTranslation("common");
const toolName = message.metadata?.tool_name as string | undefined;
const status = message.metadata?.status as string | undefined;
@@ -241,7 +246,9 @@ function MessageCard({ message }: { message: Message }) {
</div>
</div>
<div className="p-4 border-b">
<div className="text-xs text-muted-foreground mb-2 font-medium">Rendered Output:</div>
<div className="text-xs text-muted-foreground mb-2 font-medium">
{t("common:demoRenderedOutput")}
</div>
<MessageRenderer
comment={message}
isTaskDescription={false}
@@ -249,7 +256,7 @@ function MessageCard({ message }: { message: Message }) {
/>
</div>
<div className="p-4">
<JsonPanel title="Message Data" data={message} />
<JsonPanel title={t("common:demoMessageData")} data={message} />
</div>
</div>
);
@@ -262,6 +269,7 @@ function ViewModeTabs({
viewMode: ViewMode;
setViewMode: (v: ViewMode) => void;
}) {
const { t } = useTranslation("common");
return (
<div className="mb-6">
<div className="flex gap-1 p-1 bg-muted/30 rounded-lg w-fit">
@@ -275,7 +283,7 @@ function ViewModeTabs({
: "hover:bg-muted/50 text-muted-foreground"
}`}
>
{mode === "events" ? "Normalized Events" : "Test Fixtures"}
{t(mode === "events" ? "common:demoNormalizedEvents" : "common:demoTestFixtures")}
</button>
))}
</div>
@@ -298,28 +306,35 @@ function FiltersBar({
toolFilter: ToolFilter;
setToolFilter: (v: ToolFilter) => void;
}) {
const { t } = useTranslation("common");
return (
<div className="mb-6 space-y-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">
{viewMode === "events" ? "Event File:" : "Fixture File:"}
{t(viewMode === "events" ? "common:demoEventFile" : "common:demoFixtureFile")}
</span>
<select
value={selectedFile}
onChange={(e) => setSelectedFile(e.target.value)}
className="px-3 py-1.5 text-sm rounded-md border bg-background hover:bg-muted/50 transition-colors min-w-[300px]"
>
{currentFiles.length === 0 && <option value="">No files found</option>}
{currentFiles.length === 0 && <option value="">{t("common:demoNoFilesFound")}</option>}
{currentFiles.map((file) => (
<option key={file.path} value={file.path}>
{file.protocol} - {file.agent || "unknown"} ({file.message_count} messages)
{t("common:demoFileOption", {
protocol: file.protocol,
agent: file.agent || t("common:unknown"),
count: file.message_count,
})}
</option>
))}
</select>
</div>
{viewMode === "fixtures" && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Tool Type:</span>
<span className="text-sm font-medium text-muted-foreground">
{t("common:demoToolType")}
</span>
<div className="flex gap-1">
{TOOL_TABS.map((tab) => (
<button
@@ -331,7 +346,7 @@ function FiltersBar({
: "hover:bg-muted/50"
}`}
>
{tab.label}
{t(tab.labelKey)}
</button>
))}
</div>
@@ -356,6 +371,7 @@ function ContentArea({
filteredFixtures: NormalizedFixture[];
messages: Message[];
}) {
const { t } = useTranslation("common");
if (loading) {
return (
<div className="flex items-center justify-center py-12">
@@ -367,7 +383,11 @@ function ContentArea({
if (error) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-destructive">
<div className="font-medium">Error loading {viewMode}</div>
<div className="font-medium">
{t("common:demoErrorLoading", {
mode: t(viewMode === "events" ? "common:demoEvents" : "common:demoFixtures"),
})}
</div>
<div className="text-sm">{error}</div>
</div>
);
@@ -376,9 +396,7 @@ function ContentArea({
if (itemCount === 0) {
return (
<div className="text-center py-12 text-muted-foreground">
{viewMode === "events"
? "No normalized event files found. Run an agent with KANDEV_DEBUG_AGENT_MESSAGES=1 to generate event logs."
: "No fixtures found. Select a fixture file from the dropdown above."}
{t(viewMode === "events" ? "common:demoNoNormalizedEvents" : "common:demoNoFixtures")}
</div>
);
}
@@ -405,6 +423,7 @@ function ContentArea({
// --- Page ---
export default function AgentMessagesPage() {
const { t } = useTranslation("common");
const data = useAgentMessages();
return (
@@ -412,20 +431,17 @@ export default function AgentMessagesPage() {
<div className="max-w-5xl mx-auto">
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<h1 className="text-2xl font-bold">Agent Messages QA</h1>
<h1 className="text-2xl font-bold">{t("common:demoAgentMessagesQa")}</h1>
<button
onClick={data.loadContent}
disabled={data.loading || !data.selectedFile}
className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-md border hover:bg-muted/50 disabled:opacity-50 transition-colors"
>
<IconRefresh className={`h-4 w-4 ${data.loading ? "animate-spin" : ""}`} />
Refresh
{t("refresh")}
</button>
</div>
<p className="text-muted-foreground">
Visual inspection of agent message normalization. Each card shows the rendered output
alongside raw JSON.
</p>
<p className="text-muted-foreground">{t("common:demoAgentMessagesDescription")}</p>
</div>
<ViewModeTabs viewMode={data.viewMode} setViewMode={data.setViewMode} />
<FiltersBar
@@ -437,8 +453,15 @@ export default function AgentMessagesPage() {
setToolFilter={data.setToolFilter}
/>
<div className="mb-6 text-sm text-muted-foreground">
Showing {data.itemCount} of {data.totalCount}{" "}
{data.viewMode === "events" ? "messages" : "fixtures"}
{t(
data.viewMode === "events"
? "common:demoShowingMessages"
: "common:demoShowingFixtures",
{
count: data.itemCount,
total: data.totalCount,
},
)}
</div>
<ContentArea
viewMode={data.viewMode}
+4 -4
View File
@@ -3,8 +3,10 @@
import type { Message } from "@/lib/types/http";
import { MessageRenderer } from "@/components/task/chat/message-renderer";
import { dummyMessages } from "./demo-messages-data";
import { useTranslation } from "react-i18next";
export default function MessagesDemoPage() {
const { t } = useTranslation("common");
const permissionsByToolCallId = new Map<string, Message>();
const permMsg = dummyMessages.find((m) => m.id === "permission-1");
if (permMsg) {
@@ -15,10 +17,8 @@ export default function MessagesDemoPage() {
<div className="min-h-screen bg-background p-8">
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold mb-2">Message Types Demo</h1>
<p className="text-muted-foreground">
All message types rendered for styling and testing
</p>
<h1 className="text-2xl font-bold mb-2">{t("common:demoMessageTypes")}</h1>
<p className="text-muted-foreground">{t("common:demoMessageTypesDescription")}</p>
</div>
<div className="space-y-2 border rounded-lg p-5 bg-card">
@@ -1,4 +1,5 @@
import { Button } from "@kandev/ui/button";
import { useTranslation } from "react-i18next";
import type { AzureDevOpsBrowseMode } from "./azure-devops-filters";
@@ -7,13 +8,18 @@ type AzureDevOpsModeTabsProps = {
onModeChange: (mode: AzureDevOpsBrowseMode) => void;
};
const modes: Array<{ mode: AzureDevOpsBrowseMode; label: string; testId: string }> = [
{ mode: "board", label: "Board", testId: "azure-devops-board-mode" },
{ mode: "work-items", label: "Work items", testId: "azure-devops-work-items-mode" },
{ mode: "pull-requests", label: "Pull requests", testId: "azure-devops-pull-requests-mode" },
const modes: Array<{ mode: AzureDevOpsBrowseMode; labelKey: string; testId: string }> = [
{ mode: "board", labelKey: "azuredevops:board", testId: "azure-devops-board-mode" },
{ mode: "work-items", labelKey: "azuredevops:workItems", testId: "azure-devops-work-items-mode" },
{
mode: "pull-requests",
labelKey: "azuredevops:pullRequests",
testId: "azure-devops-pull-requests-mode",
},
];
export function AzureDevOpsModeTabs({ mode, onModeChange }: AzureDevOpsModeTabsProps) {
const { t } = useTranslation();
return (
<div className="flex items-center gap-1 border-b px-4 py-2">
{modes.map((option) => (
@@ -26,7 +32,7 @@ export function AzureDevOpsModeTabs({ mode, onModeChange }: AzureDevOpsModeTabsP
onClick={() => onModeChange(option.mode)}
data-testid={option.testId}
>
{option.label}
{t(option.labelKey)}
</Button>
))}
</div>
@@ -45,4 +45,28 @@ describe("Azure DevOps presets", () => {
filters: { status: "active", reviewer: "team-id", creator: "@me" },
});
});
it("translates unchanged built-in labels but preserves customized labels", () => {
const translate = (key: string) => `translated:${key}`;
expect(presetsForKind("work_item", undefined, translate)[0].label).toBe(
"translated:azuredevops:defaultQueryRecentlyUpdated",
);
const customized = presetsForKind(
"work_item",
{
workItems: [
{
id: "recent",
label: "My recent work",
group: "inbox",
filters: { wiql: "SELECT [System.Id] FROM WorkItems", top: 10 },
},
],
pullRequests: [],
},
translate,
);
expect(customized[0].label).toBe("My recent work");
});
});
@@ -12,6 +12,7 @@ import type { AzureDevOpsFiltersState } from "./azure-devops-filters";
import {
DEFAULT_AZURE_PULL_REQUEST_QUERIES,
DEFAULT_AZURE_WORK_ITEM_QUERIES,
azureQueryDisplayLabel,
} from "./azure-devops-workspace-defaults";
export type AzureDevOpsPresetKind = "work_item" | "pull_request";
@@ -75,11 +76,17 @@ export type AzureDevOpsQueryPresets = {
export function presetsForKind(
kind: AzureDevOpsPresetKind,
configured?: AzureDevOpsQueryPresets,
translate?: (key: string) => string,
): AzureDevOpsPreset[] {
const defaults =
kind === "work_item" ? DEFAULT_AZURE_WORK_ITEM_QUERIES : DEFAULT_AZURE_PULL_REQUEST_QUERIES;
const candidates = kind === "work_item" ? configured?.workItems : configured?.pullRequests;
return (candidates?.length ? candidates : defaults).map((preset) => toBrowsePreset(kind, preset));
return (candidates?.length ? candidates : defaults).map((preset) => {
const result = toBrowsePreset(kind, preset);
return translate
? { ...result, label: azureQueryDisplayLabel(kind, preset, translate) }
: result;
});
}
export const AZURE_WORK_ITEM_PRESETS = presetsForKind("work_item");
@@ -20,6 +20,7 @@ import type {
AzureDevOpsWorkItem,
} from "@/lib/types/azure-devops";
import { useTranslation } from "react-i18next";
import { azureActionDisplayCopy } from "./azure-devops-workspace-defaults";
function EmptyResult({ loading, error }: { loading: boolean; error: string | null }) {
const { t } = useTranslation();
@@ -74,14 +75,21 @@ function TaskActionsMenu<T extends { id: number }>({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{actions.map((action) => (
<DropdownMenuItem key={action.id} onSelect={() => onSelect(item, action)}>
<span className="flex flex-col">
<span>{action.label}</span>
{action.hint && <span className="text-xs text-muted-foreground">{action.hint}</span>}
</span>
</DropdownMenuItem>
))}
{actions.map((action) => {
const copy = azureActionDisplayCopy(
itemKind === "workItem" ? "work_item" : "pull_request",
action,
t,
);
return (
<DropdownMenuItem key={action.id} onSelect={() => onSelect(item, action)}>
<span className="flex flex-col">
<span>{copy.label}</span>
{copy.hint && <span className="text-xs text-muted-foreground">{copy.hint}</span>}
</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
@@ -5,6 +5,7 @@ import {
type ScopeSelection,
} from "@/components/integrations/presets-scope-bar-base";
import type { AzureDevOpsSavedView } from "@/lib/types/azure-devops";
import { useTranslation } from "react-i18next";
import {
presetsForKind,
type AzureDevOpsPresetKind,
@@ -13,11 +14,6 @@ import {
export type AzureDevOpsScopeSelection = ScopeSelection<AzureDevOpsPresetKind>;
const KINDS = [
{ value: "work_item", label: "Work items" },
{ value: "pull_request", label: "Pull requests" },
] as const;
export function AzureDevOpsScopeBar({
selected,
onSelect,
@@ -35,14 +31,19 @@ export function AzureDevOpsScopeBar({
onSaveCurrent: () => void;
queryPresets: AzureDevOpsQueryPresets;
}) {
const { t } = useTranslation();
const kinds = [
{ value: "work_item" as const, label: t("azuredevops:workItems") },
{ value: "pull_request" as const, label: t("azuredevops:pullRequests") },
];
return (
<IntegrationScopeBar
testId="azure-devops-presets-scope-bar"
savedMenuTestId="azure-devops-saved-views-menu"
kinds={KINDS}
kinds={kinds}
selected={selected}
onSelect={onSelect}
presetsByKind={(kind) => presetsForKind(kind, queryPresets)}
presetsByKind={(kind) => presetsForKind(kind, queryPresets, t)}
savedPresets={savedViews}
onDeleteSaved={onDeleteSaved}
canSaveCurrent={canSaveCurrent}
@@ -30,10 +30,10 @@ function taskPullRequest(
describe("getAzureDevOpsPullRequestPresentation", () => {
it("prioritizes terminal Azure states", () => {
expect(getAzureDevOpsPullRequestPresentation(taskPullRequest({ status: "completed" }))).toEqual(
{ provider: "azure_devops", label: "Completed", tone: "success" },
{ provider: "azure_devops", labelKey: "azuredevops:prStatusCompleted", tone: "success" },
);
expect(getAzureDevOpsPullRequestPresentation(taskPullRequest({ status: "abandoned" }))).toEqual(
{ provider: "azure_devops", label: "Abandoned", tone: "muted" },
{ provider: "azure_devops", labelKey: "azuredevops:prStatusAbandoned", tone: "muted" },
);
});
@@ -42,20 +42,24 @@ describe("getAzureDevOpsPullRequestPresentation", () => {
getAzureDevOpsPullRequestPresentation(
taskPullRequest({ policyState: "failure", reviewState: "approved" }),
),
).toEqual({ provider: "azure_devops", label: "Policy failed", tone: "danger" });
).toEqual({
provider: "azure_devops",
labelKey: "azuredevops:prStatusPolicyFailed",
tone: "danger",
});
});
it("distinguishes drafts, review waits, and ready pull requests", () => {
expect(getAzureDevOpsPullRequestPresentation(taskPullRequest({ isDraft: true })).label).toBe(
"Draft",
expect(getAzureDevOpsPullRequestPresentation(taskPullRequest({ isDraft: true })).labelKey).toBe(
"azuredevops:prStatusDraft",
);
expect(
getAzureDevOpsPullRequestPresentation(taskPullRequest({ reviewState: "waiting" })).label,
).toBe("Waiting for review");
getAzureDevOpsPullRequestPresentation(taskPullRequest({ reviewState: "waiting" })).labelKey,
).toBe("azuredevops:prStatusWaitingForReview");
expect(
getAzureDevOpsPullRequestPresentation(
taskPullRequest({ reviewState: "approved", policyState: "success" }),
),
).toEqual({ provider: "azure_devops", label: "Ready", tone: "success" });
).toEqual({ provider: "azure_devops", labelKey: "azuredevops:prStatusReady", tone: "success" });
});
});
@@ -2,7 +2,7 @@ import type { AzureDevOpsTaskPullRequest } from "@/lib/types/azure-devops";
export type AzureDevOpsPullRequestPresentation = {
provider: "azure_devops";
label: string;
labelKey: string;
tone: "success" | "danger" | "warning" | "muted" | "info";
};
@@ -11,28 +11,44 @@ export function getAzureDevOpsPullRequestPresentation(
): AzureDevOpsPullRequestPresentation {
const status = pullRequest.status.toLowerCase();
if (status === "completed") {
return { provider: "azure_devops", label: "Completed", tone: "success" };
return { provider: "azure_devops", labelKey: "azuredevops:prStatusCompleted", tone: "success" };
}
if (status === "abandoned") {
return { provider: "azure_devops", label: "Abandoned", tone: "muted" };
return { provider: "azure_devops", labelKey: "azuredevops:prStatusAbandoned", tone: "muted" };
}
if (pullRequest.policyState === "failure") {
return { provider: "azure_devops", label: "Policy failed", tone: "danger" };
return {
provider: "azure_devops",
labelKey: "azuredevops:prStatusPolicyFailed",
tone: "danger",
};
}
if (pullRequest.reviewState === "rejected") {
return { provider: "azure_devops", label: "Changes requested", tone: "danger" };
return {
provider: "azure_devops",
labelKey: "azuredevops:prStatusChangesRequested",
tone: "danger",
};
}
if (pullRequest.isDraft) {
return { provider: "azure_devops", label: "Draft", tone: "muted" };
return { provider: "azure_devops", labelKey: "azuredevops:prStatusDraft", tone: "muted" };
}
if (pullRequest.policyState === "pending") {
return { provider: "azure_devops", label: "Policy running", tone: "warning" };
return {
provider: "azure_devops",
labelKey: "azuredevops:prStatusPolicyRunning",
tone: "warning",
};
}
if (pullRequest.reviewState === "waiting") {
return { provider: "azure_devops", label: "Waiting for review", tone: "warning" };
return {
provider: "azure_devops",
labelKey: "azuredevops:prStatusWaitingForReview",
tone: "warning",
};
}
if (pullRequest.reviewState === "approved" && pullRequest.policyState === "success") {
return { provider: "azure_devops", label: "Ready", tone: "success" };
return { provider: "azure_devops", labelKey: "azuredevops:prStatusReady", tone: "success" };
}
return { provider: "azure_devops", label: "Active", tone: "info" };
return { provider: "azure_devops", labelKey: "azuredevops:prStatusActive", tone: "info" };
}
@@ -3,6 +3,7 @@
import { IconBrandAzure, IconExternalLink } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useTranslation } from "react-i18next";
import { useAppStore } from "@/components/state-provider";
import { useAzureDevOpsTaskPullRequests } from "@/hooks/domains/azure-devops/use-azure-devops-task-pull-requests";
import { getAzureDevOpsPullRequestPresentation } from "./azure-devops-status";
@@ -16,13 +17,19 @@ const TONE_CLASS = {
};
export function AzureDevOpsTaskPullRequestChip({ taskId }: { taskId: string | null }) {
const { t } = useTranslation();
const workspaceId = useAppStore((state) => state.workspaces.activeId);
const pullRequests = useAzureDevOpsTaskPullRequests(workspaceId, taskId);
const first = pullRequests[0];
if (!first) return null;
const presentation = getAzureDevOpsPullRequestPresentation(first);
const statusLabel = t(presentation.labelKey);
const suffix = pullRequests.length > 1 ? ` +${pullRequests.length - 1}` : "";
const label = `Azure PR ${first.pullRequestId}: ${presentation.label}${suffix}`;
const label = t("azuredevops:pullRequestStatusAria", {
id: first.pullRequestId,
status: statusLabel,
suffix,
});
return (
<Tooltip>
@@ -36,7 +43,7 @@ export function AzureDevOpsTaskPullRequestChip({ taskId }: { taskId: string | nu
<a href={first.pullRequestUrl} target="_blank" rel="noreferrer" aria-label={label}>
<IconBrandAzure className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">PR {first.pullRequestId}</span>
<span className="hidden truncate sm:inline">{presentation.label}</span>
<span className="hidden truncate sm:inline">{statusLabel}</span>
{suffix && <span className="shrink-0">{suffix}</span>}
<IconExternalLink className="h-3 w-3 shrink-0" />
</a>
@@ -44,7 +51,7 @@ export function AzureDevOpsTaskPullRequestChip({ taskId }: { taskId: string | nu
</TooltipTrigger>
<TooltipContent className="max-w-80">
<p className="font-medium">{first.title}</p>
<p>{presentation.label}</p>
<p>{statusLabel}</p>
</TooltipContent>
</Tooltip>
);
@@ -28,6 +28,7 @@ import type {
} from "@/lib/types/azure-devops";
import { markdownComponents, remarkPlugins } from "@/components/shared/markdown-components";
import { useTranslation } from "react-i18next";
import { azureActionDisplayCopy } from "./azure-devops-workspace-defaults";
type BoardContext = {
board: AzureDevOpsBoard;
@@ -215,18 +216,21 @@ function QuickActions({
<div className="space-y-2" data-testid="azure-work-item-quick-actions">
<Label>{t("azuredevops:taskActions")}</Label>
<div className="flex flex-wrap gap-2">
{actions.map((action) => (
<Button
key={action.id}
type="button"
variant="outline"
className="min-h-11 cursor-pointer"
onClick={() => onStartTask(item, action)}
>
<IconPlus className="h-4 w-4" />
{action.label}
</Button>
))}
{actions.map((action) => {
const copy = azureActionDisplayCopy("work_item", action, t);
return (
<Button
key={action.id}
type="button"
variant="outline"
className="min-h-11 cursor-pointer"
onClick={() => onStartTask(item, action)}
>
<IconPlus className="h-4 w-4" />
{copy.label}
</Button>
);
})}
</div>
</div>
);
@@ -24,6 +24,8 @@
*/
import type { AzureDevOpsActionPreset, AzureDevOpsQueryPreset } from "@/lib/types/azure-devops";
type Translate = (key: string) => string;
const WIQL_START = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project";
const WIQL_ORDER = " ORDER BY [System.ChangedDate] DESC";
@@ -139,3 +141,68 @@ export const DEFAULT_AZURE_PULL_REQUEST_ACTIONS: AzureDevOpsActionPreset[] = [
promptTemplate: "Investigate and fix CI failures for the Azure DevOps pull request at {{url}}.",
},
];
const QUERY_LABEL_KEYS: Record<string, string> = {
"work_item:recent": "azuredevops:defaultQueryRecentlyUpdated",
"work_item:assigned": "azuredevops:defaultQueryAssignedToMe",
"work_item:active": "azuredevops:defaultQueryActive",
"work_item:created": "azuredevops:defaultQueryCreatedByMe",
"pull_request:review-requested": "azuredevops:defaultQueryReviewRequested",
"pull_request:active": "azuredevops:defaultQueryOpen",
"pull_request:completed": "azuredevops:defaultQueryCompleted",
"pull_request:created": "azuredevops:defaultQueryCreatedByMe",
};
const ACTION_COPY_KEYS: Record<string, { labelKey: string; hintKey: string }> = {
"work_item:implement": {
labelKey: "azuredevops:defaultActionImplement",
hintKey: "azuredevops:defaultActionImplementHint",
},
"work_item:investigate": {
labelKey: "azuredevops:defaultActionInvestigate",
hintKey: "azuredevops:defaultActionInvestigateHint",
},
"work_item:reproduce": {
labelKey: "azuredevops:defaultActionReproduce",
hintKey: "azuredevops:defaultActionReproduceHint",
},
"pull_request:review": {
labelKey: "azuredevops:defaultActionReview",
hintKey: "azuredevops:defaultActionReviewHint",
},
"pull_request:address-feedback": {
labelKey: "azuredevops:defaultActionAddressFeedback",
hintKey: "azuredevops:defaultActionAddressFeedbackHint",
},
"pull_request:fix-ci": {
labelKey: "azuredevops:defaultActionFixCi",
hintKey: "azuredevops:defaultActionFixCiHint",
},
};
export function azureQueryDisplayLabel(
kind: "work_item" | "pull_request",
preset: AzureDevOpsQueryPreset,
t: Translate,
): string {
const defaults =
kind === "work_item" ? DEFAULT_AZURE_WORK_ITEM_QUERIES : DEFAULT_AZURE_PULL_REQUEST_QUERIES;
const baseline = defaults.find((candidate) => candidate.id === preset.id);
const key = QUERY_LABEL_KEYS[`${kind}:${preset.id}`];
return key && baseline?.label === preset.label ? t(key) : preset.label;
}
export function azureActionDisplayCopy(
kind: "work_item" | "pull_request",
action: AzureDevOpsActionPreset,
t: Translate,
): { label: string; hint: string } {
const defaults =
kind === "work_item" ? DEFAULT_AZURE_WORK_ITEM_ACTIONS : DEFAULT_AZURE_PULL_REQUEST_ACTIONS;
const baseline = defaults.find((candidate) => candidate.id === action.id);
const keys = ACTION_COPY_KEYS[`${kind}:${action.id}`];
if (!keys || baseline?.label !== action.label || baseline.hint !== action.hint) {
return { label: action.label, hint: action.hint };
}
return { label: t(keys.labelKey), hint: t(keys.hintKey) };
}
@@ -1,6 +1,7 @@
"use client";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { Icon } from "@tabler/icons-react";
import { TaskCreateDialog } from "@/components/task-create-dialog";
import { useAppStore } from "@/components/state-provider";
@@ -101,6 +102,7 @@ export function QuickTaskLauncher({
}: QuickTaskLauncherProps) {
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation("gitlab");
const setTaskMR = useAppStore((state) => state.setTaskMR);
const workflow = workflows[0];
const workflowSteps = useMemo(
@@ -132,10 +134,10 @@ export function QuickTaskLauncher({
setTaskMR(workspaceId, task.id, association);
} catch (error) {
toast({
title: "Task created, but merge request was not linked",
description: `Open the task and use Link GitLab merge request to retry. ${
error instanceof Error ? error.message : "GitLab rejected the association."
}`,
title: t("gitlab:taskCreatedMrNotLinked"),
description: t("gitlab:taskCreatedMrNotLinkedDescription", {
error: error instanceof Error ? error.message : t("gitlab:gitlabRejectedAssociation"),
}),
variant: "error",
duration: 8000,
});
@@ -22,7 +22,7 @@ describe("getStepAdjacency", () => {
];
const stepsWithOrphan: WorkflowStep[] = [
...realSteps,
{ id: ORPHAN_STEP_ID, title: "Needs Reassignment", color: "#f59e0b" },
{ id: ORPHAN_STEP_ID, title: "", color: "#f59e0b" },
];
it("offers both neighbors for a middle real step with no orphan present", () => {
@@ -12,12 +12,13 @@ import { useTranslation } from "react-i18next";
export function getGraph2DisplayState(
tasks: Task[],
steps: WorkflowStep[],
orphanStepTitle: string,
): { displayTasks: Task[]; displaySteps: WorkflowStep[] } {
const stepIds = new Set(steps.map((step) => step.id));
const { tasks: displayTasks, hasOrphans } = remapOrphanTasks(tasks, stepIds, ORPHAN_STEP_ID);
return {
displayTasks,
displaySteps: hasOrphans ? [...steps, ORPHAN_STEP] : steps,
displaySteps: hasOrphans ? [...steps, { ...ORPHAN_STEP, title: orphanStepTitle }] : steps,
};
}
@@ -42,8 +43,8 @@ export function SwimlaneGraph2Content({
});
const [movingTaskId, setMovingTaskId] = useState<string | null>(null);
const { displayTasks, displaySteps } = useMemo(
() => getGraph2DisplayState(tasks, steps),
[tasks, steps],
() => getGraph2DisplayState(tasks, steps, t("kanban:needsReassignment")),
[tasks, steps, t],
);
const sortedTasks = useMemo(
@@ -23,6 +23,7 @@ describe("getGraph2DisplayState", () => {
const { displayTasks, displaySteps } = getGraph2DisplayState(
[makeTask("valid", "todo"), makeTask("orphan", "deleted-step")],
steps,
"Needs Reassignment",
);
expect(displaySteps.map((step) => step.title)).toEqual(["Todo", "Done", "Needs Reassignment"]);
@@ -34,6 +35,7 @@ describe("getGraph2DisplayState", () => {
const { displayTasks, displaySteps } = getGraph2DisplayState(
[makeTask("valid", "todo"), makeTask("finished", "done")],
steps,
"Needs Reassignment",
);
expect(displaySteps).toBe(steps);
@@ -43,7 +43,7 @@ export const ORPHAN_STEP_ID = "__kandev_orphan__";
export const ORPHAN_STEP: WorkflowStep = {
id: ORPHAN_STEP_ID,
title: "Needs Reassignment",
title: "",
color: "#f59e0b",
};
@@ -234,12 +234,15 @@ function useOrphanDisplay(
tasks: Task[],
steps: WorkflowStep[],
): { displayTasks: Task[]; displaySteps: WorkflowStep[] } {
const { t } = useTranslation("kanban");
return useMemo(() => {
const stepIds = new Set(steps.map((s) => s.id));
const { tasks: displayTasks, hasOrphans } = remapOrphanTasks(tasks, stepIds, ORPHAN_STEP_ID);
const displaySteps = hasOrphans ? [...steps, ORPHAN_STEP] : steps;
const displaySteps = hasOrphans
? [...steps, { ...ORPHAN_STEP, title: t("kanban:needsReassignment") }]
: steps;
return { displayTasks, displaySteps };
}, [tasks, steps]);
}, [tasks, steps, t]);
}
function useTasksByStep(tasks: Task[]) {
+4 -4
View File
@@ -309,17 +309,17 @@ function useReviewDialogHandlers(opts: ReviewDialogHandlerOptions) {
try {
const result = await discard([path], repositoryName);
if (result.success)
toast({ title: "Changes discarded", description: path, variant: "success" });
toast({ title: t("review:changesDiscarded"), description: path, variant: "success" });
else
toast({
title: "Discard failed",
title: t("review:discardFailed"),
description: result.error || t("common:anErrorOccurred"),
variant: "error",
});
} catch (e) {
toast({
title: "Discard failed",
description: e instanceof Error ? e.message : "An error occurred",
title: t("review:discardFailed"),
description: e instanceof Error ? e.message : t("common:anErrorOccurred"),
variant: "error",
});
}
@@ -231,6 +231,21 @@ function buildConflictLabels(groups: ShortcutConflictGroup[]): Map<string, strin
const CONFIGURABLE_SHORTCUT_IDS = Object.keys(CONFIGURABLE_SHORTCUTS) as ConfigurableShortcutId[];
function useShortcutConflictLabels(
pluginEntries: ShortcutEntry[],
overrides: StoredShortcutOverrides,
translate: NonNullable<Parameters<typeof coreShortcutEntries>[0]>,
) {
return useMemo(() => {
const allEntries = [...coreShortcutEntries(translate), ...pluginEntries];
const resolved = allEntries.map((entry) => ({
entry,
shortcut: resolveShortcutEntry(entry, overrides),
}));
return buildConflictLabels(findShortcutConflicts(resolved, isMac()));
}, [pluginEntries, overrides, translate]);
}
export function KeyboardShortcutsCard({
overrides,
baselineOverrides = {},
@@ -247,15 +262,7 @@ export function KeyboardShortcutsCard({
const shortcuts = resolveAllShortcuts(overrides);
const baselineShortcuts = resolveAllShortcuts(baselineOverrides);
const allEntries = useMemo(() => [...coreShortcutEntries(), ...pluginEntries], [pluginEntries]);
const conflictLabels = useMemo(() => {
const resolved = allEntries.map((entry) => ({
entry,
shortcut: resolveShortcutEntry(entry, overrides),
}));
return buildConflictLabels(findShortcutConflicts(resolved, isMac()));
}, [allEntries, overrides]);
const conflictLabels = useShortcutConflictLabels(pluginEntries, overrides, t);
const handleChange = useCallback(
(id: string, shortcut: KeyboardShortcut) => {
@@ -294,7 +301,7 @@ export function KeyboardShortcutsCard({
<ShortcutRecorder
key={id}
shortcutId={id}
label={CONFIGURABLE_SHORTCUTS[id].label}
label={t(CONFIGURABLE_SHORTCUTS[id].labelKey)}
defaultShortcut={CONFIGURABLE_SHORTCUTS[id].default}
current={shortcuts[id]}
onChange={handleChange}
+10 -3
View File
@@ -61,6 +61,7 @@ function useMermaidRender(
resolvedTheme: string | undefined,
toast: ToastFn,
taskId: string | null,
errorTitle: string,
) {
const [svg, setSvg] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -105,7 +106,7 @@ function useMermaidRender(
// screen — the error banner is also suppressed in that case, so a
// toast here would surface a failure the user never sees in the UI.
if (svgRef.current === null) {
showMermaidErrorToast(toast, taskId, err.message);
showMermaidErrorToast(toast, taskId, err.message, errorTitle);
}
});
}, RENDER_DEBOUNCE_MS);
@@ -114,7 +115,7 @@ function useMermaidRender(
cancelled = true;
clearTimeout(timer);
};
}, [code, resolvedTheme, taskId, toast]);
}, [code, errorTitle, resolvedTheme, taskId, toast]);
return { svg, error };
}
@@ -128,7 +129,13 @@ export function MermaidBlock({ code, taskId }: MermaidBlockProps) {
const [showCode, setShowCode] = useState(false);
const { resolvedTheme } = useTheme();
const { toast } = useToast();
const { svg, error } = useMermaidRender(code, resolvedTheme, toast, taskId ?? null);
const { svg, error } = useMermaidRender(
code,
resolvedTheme,
toast,
taskId ?? null,
t("common:failedToRenderDiagram"),
);
const viewportWidth = useMermaidViewportWidth(scrollRegionElement);
const { scale, zoomIn, zoomOut, zoomReset, resetAutoScale } = useMermaidScale(
svgSize,
@@ -57,7 +57,7 @@ describe("useMermaidErrorToast", () => {
detail: { message: "Plan parser error again", taskId: "task-1" },
}),
);
showMermaidErrorToast(mockToast, "task-1", "Chat parser error");
showMermaidErrorToast(mockToast, "task-1", "Chat parser error", "Failed to render diagram");
});
expect(mockToast).toHaveBeenCalledOnce();
@@ -1,6 +1,7 @@
"use client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useToast } from "@/components/toast-provider";
import { MERMAID_ERROR_EVENT } from "./mermaid-utils";
@@ -13,13 +14,14 @@ export function showMermaidErrorToast(
toast: ToastFn,
taskId: string | null | undefined,
message: string,
title: string,
): void {
if (taskId) {
if (notifiedTaskIds.has(taskId)) return;
notifiedTaskIds.add(taskId);
}
toast({ title: "Failed to render diagram", description: message, variant: "error" });
toast({ title, description: message, variant: "error" });
}
export function resetMermaidErrorToastHistoryForTest(): void {
@@ -28,13 +30,14 @@ export function resetMermaidErrorToastHistoryForTest(): void {
export function useMermaidErrorToast(): void {
const { toast } = useToast();
const { t } = useTranslation("common");
useEffect(() => {
const handler = (e: Event) => {
const detail = (e as CustomEvent<MermaidErrorDetail>).detail;
showMermaidErrorToast(toast, detail?.taskId, detail?.message);
showMermaidErrorToast(toast, detail?.taskId, detail?.message, t("failedToRenderDiagram"));
};
document.addEventListener(MERMAID_ERROR_EVENT, handler);
return () => document.removeEventListener(MERMAID_ERROR_EVENT, handler);
}, [toast]);
}, [t, toast]);
}
@@ -21,6 +21,7 @@ import {
} from "@/components/task-create-dialog-autopick";
import { useRepositoryAutoSelectEffect } from "@/components/task-create-dialog-repository-autopick";
import { createDebugLogger, isDebug } from "@/lib/debug/log";
import { t } from "@/lib/i18n";
// Re-export autopick hooks for callers that imported them from this module.
export { useWorkflowAgentProfileEffect };
@@ -92,8 +93,8 @@ export function useDiscoverReposEffect(
})
.catch((e) => {
toast({
title: "Failed to discover repositories",
description: e instanceof Error ? e.message : "Request failed",
title: t("task:failedToDiscoverRepositories"),
description: e instanceof Error ? e.message : t("common:requestFailed"),
variant: "error",
});
setDiscoveredRepositories([]);
@@ -6,6 +6,7 @@ import { ApiError } from "@/lib/api/client";
import { createTask as createTaskREST } from "@/lib/api";
import type { useToast } from "@/components/toast-provider";
import type { TaskCreateSubmit } from "@/components/task-create-dialog-types";
import { t } from "@/lib/i18n";
type CreateTaskParams = Parameters<typeof createTaskREST>[0];
type CreateTaskResponse = Awaited<ReturnType<typeof createTaskREST>>;
@@ -84,8 +85,8 @@ export function useFreshBranchConsent({
return await promptForList(status.dirty_files);
} catch (error) {
toast({
title: "Failed to check local repository status",
description: error instanceof Error ? error.message : "Request failed",
title: t("task:failedToCheckRepositoryStatus"),
description: error instanceof Error ? error.message : t("common:requestFailed"),
variant: "error",
});
return null;
@@ -11,9 +11,12 @@ import { buildStartRequest } from "@/lib/services/session-launch-helpers";
import { useToast } from "@/components/toast-provider";
import { linkToTask } from "@/lib/links";
import type { SubmitHandlersDeps } from "@/components/task-create-dialog-types";
import { t } from "@/lib/i18n";
import { useFreshBranchConsent } from "@/components/task-create-dialog-fresh-branch-consent";
import { queueTaskCreateLastUsedFromPayload } from "@/components/task-create-dialog-handlers";
const GENERIC_ERROR_KEY = "common:anErrorOccurred";
import {
activatePlanMode,
buildCreateTaskPayload,
@@ -27,18 +30,14 @@ import {
} from "@/components/task-create-dialog-helpers";
import { hasRegisteredRepositoryProviderCandidate } from "@/lib/plugins/repository-provider-url-resolution";
const GENERIC_ERROR_MESSAGE = "An error occurred";
const DUPLICATE_REPO_TITLE = "Duplicate repository";
const UNRESOLVED_REPO_TITLE = "Repository is still being verified";
function notifyQueuedTask(
response: { queued_for_step_id?: string | null },
notify: (input: { title: string; description: string }) => unknown,
) {
if (!response.queued_for_step_id) return;
notify({
title: "Task queued",
description: "The workflow step is at its WIP limit; this task will start when capacity opens.",
title: t("task:taskQueued"),
description: t("task:taskQueuedWipLimit"),
});
}
@@ -168,8 +167,8 @@ export function useTaskSubmitHandlers({
const duplicate = findDuplicateRemoteRepo(remoteRepos);
if (!duplicate) return false;
toast({
title: DUPLICATE_REPO_TITLE,
description: `${duplicate} is added more than once - remove the duplicate row.`,
title: t("task:duplicateRepository"),
description: t("task:duplicateRepositoryDescription", { repository: duplicate }),
variant: "error",
});
return true;
@@ -188,10 +187,10 @@ export function useTaskSubmitHandlers({
if (!unresolved) return false;
const resolutionError = prInfoByUrl.error(unresolved.url);
toast({
title: UNRESOLVED_REPO_TITLE,
title: t("task:repositoryStillBeingVerified"),
description: resolutionError
? "Kandev could not verify this provider URL. Use Retry on the repository row."
: "Wait for provider inspection to finish before creating the task.",
? t("task:repositoryProviderVerificationFailed")
: t("task:repositoryProviderVerificationPending"),
variant: "error",
});
return true;
@@ -240,8 +239,7 @@ export function useTaskSubmitHandlers({
freshBranch: buildFreshBranchPayload(consentedDirtyFiles),
});
},
// buildFreshBranchPayload is a closure over current scope; lint exception kept narrow.
// eslint-disable-next-line react-hooks/exhaustive-deps
// buildFreshBranchPayload is a closure over current scope; dependencies stay explicit below.
[
noRepository,
useRemote,
@@ -290,8 +288,8 @@ export function useTaskSubmitHandlers({
router.push(linkToTask(taskId));
} catch (error) {
toast({
title: "Failed to create session",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToCreateSession"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -357,8 +355,8 @@ export function useTaskSubmitHandlers({
onSuccess?.(updatedTask, "edit", { taskSessionId });
} catch (error) {
toast({
title: "Failed to update task",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToUpdateTask"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -386,8 +384,8 @@ export function useTaskSubmitHandlers({
onSuccess?.(result.updatedTask, "edit");
} catch (error) {
toast({
title: "Failed to update task",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToUpdateTask"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -538,8 +536,8 @@ export function useTaskSubmitHandlers({
await performEditWithPlanMode();
} catch (error) {
toast({
title: "Failed to start task in plan mode",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToStartTaskPlanMode"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -569,8 +567,8 @@ export function useTaskSubmitHandlers({
});
} catch (error) {
toast({
title: "Failed to start task in plan mode",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToStartTaskPlanMode"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -637,8 +635,8 @@ export function useTaskSubmitHandlers({
await submitCreateTask({ trimmedTitle, trimmedDescription, consent, attachments });
} catch (error) {
toast({
title: "Failed to create task",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToCreateTask"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -706,8 +704,8 @@ export function useTaskSubmitHandlers({
onOpenChange(false);
} catch (error) {
toast({
title: "Failed to create task",
description: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
title: t("task:failedToCreateTask"),
description: error instanceof Error ? error.message : t(GENERIC_ERROR_KEY),
variant: "error",
});
} finally {
@@ -9,6 +9,7 @@ import type { TaskFormInputsHandle } from "@/components/task-create-dialog-types
import type { AgentProfileOption } from "@/lib/state/slices";
import type { SummarizeSessionResult } from "@/hooks/use-summarize-session";
import { applySummarizeSessionResult, type SummaryToastFn } from "./session-context-summary";
import { t } from "@/lib/i18n";
type SessionContextChangeOpts = {
promptRef: RefObject<TaskFormInputsHandle | null>;
@@ -107,8 +108,8 @@ export function useSessionLaunchSubmit({
onClose();
} catch (error) {
toast({
title: "Failed to create session",
description: error instanceof Error ? error.message : "Unknown error",
title: t("task:failedToCreateSession"),
description: error instanceof Error ? error.message : t("common:unknownError"),
variant: "error",
});
} finally {
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useToast } from "@/components/toast-provider";
import { useAppStore } from "@/components/state-provider";
import { useTaskPR } from "@/hooks/domains/github/use-task-pr";
@@ -19,14 +20,15 @@ import { useTaskReviewShortcut } from "./use-task-review-shortcut";
*/
export function TaskPRShortcut({ taskId }: { taskId: string | null }) {
const { toast } = useToast();
const { t } = useTranslation("task");
const { prs } = useTaskPR(taskId);
const mrs = useTaskMRs(taskId);
const overrides = useAppStore((s) => s.userSettings.keyboardShortcuts);
const shortcut = getShortcut("OPEN_TASK_PR", overrides);
const targets = useMemo(() => buildTaskReviewTargets(prs, mrs), [mrs, prs]);
const onNoTargets = useCallback(
() => toast({ description: "No pull request or merge request linked to this task" }),
[toast],
() => toast({ description: t("task:noPullOrMergeRequestLinked") }),
[t, toast],
);
const onOpenTarget = useCallback((target: TaskReviewTarget) => {
void openExternalLink(target.url).catch(() => undefined);
+28
View File
@@ -1901,4 +1901,32 @@ export const i18nGuardFiles = [
// comment deferred the `label`/`keywords` to "the components/task migration".
// Both are palette copy and belong with the rest of it.
"components/task/recent-task-switcher-hooks.ts",
// Second-audit gaps hidden in hook toasts, configuration objects, synthetic
// display records, and developer QA page chrome rather than JSX text.
"app/demo/agent-messages/page.tsx",
"app/demo/messages/page.tsx",
"components/azure-devops/azure-devops-mode-tabs.tsx",
"components/azure-devops/azure-devops-scope-bar.tsx",
"components/azure-devops/azure-devops-task-pull-request-chip.tsx",
"components/gitlab/my-gitlab/quick-task-launcher.tsx",
"components/shared/mermaid-error-toast.tsx",
"components/task/task-pr-shortcut.tsx",
"components/task-create-dialog-effects.ts",
"components/task-create-dialog-fresh-branch-consent.ts",
"components/task-create-dialog-submit.tsx",
"hooks/domains/kanban/use-implement-fresh.ts",
"hooks/domains/kanban/use-plan-actions.ts",
"hooks/domains/review/use-finding-actions.ts",
"hooks/domains/review/use-send-finding-to-agent.ts",
"hooks/domains/session/use-commit-diff.ts",
"hooks/domains/session/use-request-changes-walkthrough.ts",
"hooks/domains/session/use-session-actions.ts",
"hooks/domains/session/use-task-plan.ts",
"hooks/use-file-editors.ts",
"hooks/use-file-operations.ts",
"hooks/use-nest-task.ts",
"hooks/use-open-session-in-editor.ts",
"hooks/use-sidebar-views-sync.ts",
"hooks/use-task-workflow-move.ts",
"lib/keyboard/shortcut-overrides.ts",
];
@@ -1,5 +1,6 @@
import type React from "react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { setChatDraftContent } from "@/lib/local-storage";
@@ -51,6 +52,7 @@ export function useImplementFresh(
const setActiveSession = useAppStore((s) => s.setActiveSession);
const setTaskPlan = useAppStore((s) => s.setTaskPlan);
const { toast } = useToast();
const { t } = useTranslation("task");
return useCallback(async () => {
if (!taskId || !resolvedSessionId || !planningSession?.agent_profile_id) {
@@ -87,7 +89,7 @@ export function useImplementFresh(
return true;
} catch (err) {
console.error("Failed to launch fresh implementation session:", err);
toast({ description: "Failed to start implementation session", variant: "error" });
toast({ description: t("task:failedToStartImplementationSession"), variant: "error" });
return false;
}
}, [
@@ -98,5 +100,6 @@ export function useImplementFresh(
setTaskPlan,
setActiveSession,
toast,
t,
]);
}
@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { getWebSocketClient } from "@/lib/ws/connection";
@@ -21,6 +22,7 @@ const AUTO_TRANSITION_ACTIONS = ["move_to_next", "move_to_previous", "move_to_st
export function useNextWorkflowStep(taskId: string | null) {
const { toast } = useToast();
const { t } = useTranslation("task");
const workflowId = useAppStore((s) => s.kanban.workflowId);
const steps = useAppStore((s) => s.kanban.steps);
const taskStepId = useAppStore((s) => {
@@ -83,11 +85,11 @@ export function useNextWorkflowStep(taskId: string | null) {
return true;
} catch (err) {
console.error("Failed to proceed to next step:", err);
toast({ description: "Failed to proceed to next step", variant: "error" });
toast({ description: t("task:failedToProceedToNextStep"), variant: "error" });
setMoveFromSessionId(null);
return false;
}
}, [taskId, workflowId, nextStep, activeSessionId, toast]);
}, [taskId, workflowId, nextStep, activeSessionId, t, toast]);
const proceedStepName = nextStep && !currentStepAutoTransitions ? nextStep.title : null;
@@ -158,6 +160,7 @@ function useImplementPlan(
) {
const setTaskPlan = useAppStore((s) => s.setTaskPlan);
const { toast } = useToast();
const { t } = useTranslation("task");
return useCallback(async (): Promise<boolean> => {
if (!resolvedSessionId || !taskId) return false;
@@ -209,7 +212,7 @@ function useImplementPlan(
return true;
} catch (err) {
console.error("Failed to start implementation:", err);
toast({ description: "Failed to start implementing the plan", variant: "error" });
toast({ description: t("task:failedToStartImplementingPlan"), variant: "error" });
return false;
}
}, [
@@ -220,6 +223,7 @@ function useImplementPlan(
clearPlanModeAfterSend,
handlePlanModeChange,
toast,
t,
]);
}
@@ -1,6 +1,7 @@
"use client";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAppStoreApi } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { updateReviewFindingStatus } from "@/lib/api/domains/review-api";
@@ -16,6 +17,7 @@ import type { ReviewFindingStatus, TaskReviewFinding } from "@/lib/types/review"
export function useFindingActions(taskId: string | null | undefined) {
const storeApi = useAppStoreApi();
const { toast } = useToast();
const { t } = useTranslation("review");
const setStatus = useCallback(
async (finding: TaskReviewFinding, status: ReviewFindingStatus) => {
@@ -28,13 +30,13 @@ export function useFindingActions(taskId: string | null | undefined) {
} catch (error) {
storeApi.getState().updateReviewFinding(taskId, previous);
toast({
title: "Could not update finding",
description: error instanceof Error ? error.message : "An error occurred",
title: t("review:couldNotUpdateFinding"),
description: error instanceof Error ? error.message : t("common:anErrorOccurred"),
variant: "error",
});
}
},
[taskId, storeApi, toast],
[taskId, storeApi, t, toast],
);
return {
@@ -1,6 +1,7 @@
"use client";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAppStoreApi } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { appendToQueue } from "@/lib/api/domains/queue-api";
@@ -29,6 +30,7 @@ function isAgentBusy(state: string | undefined): boolean {
export function useSendFindingToAgent({ taskId, sessionId }: Params) {
const storeApi = useAppStoreApi();
const { toast } = useToast();
const { t } = useTranslation("review");
return useCallback(
async (finding: TaskReviewFinding) => {
@@ -46,7 +48,7 @@ export function useSendFindingToAgent({ taskId, sessionId }: Params) {
content,
...(planMode ? { plan_mode: true } : {}),
});
toast({ title: "Finding queued for the agent", variant: "success" });
toast({ title: t("review:findingQueuedForAgent"), variant: "success" });
return;
}
const client = getWebSocketClient();
@@ -66,15 +68,15 @@ export function useSendFindingToAgent({ taskId, sessionId }: Params) {
// Add the returned message directly so the chat updates even if the
// broadcast is missed; addMessage is idempotent on id.
if (created?.id && created.session_id) storeApi.getState().addMessage(created);
toast({ title: "Finding sent to the agent", variant: "success" });
toast({ title: t("review:findingSentToAgent"), variant: "success" });
} catch (error) {
toast({
title: "Could not send the finding",
description: error instanceof Error ? error.message : "An error occurred",
title: t("review:couldNotSendFinding"),
description: error instanceof Error ? error.message : t("common:anErrorOccurred"),
variant: "error",
});
}
},
[taskId, sessionId, storeApi, toast],
[taskId, sessionId, storeApi, t, toast],
);
}
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { requestCommitDiff } from "@/components/task/commit-diff-request";
@@ -29,6 +30,7 @@ export function useCommitDiff(commitSha: string, repo?: string): UseCommitDiffRe
: false,
);
const { toast } = useToast();
const { t } = useTranslation("task");
const [files, setFiles] = useState<Record<string, FileInfo> | null>(null);
const [loading, setLoading] = useState(false);
@@ -57,8 +59,8 @@ export function useCommitDiff(commitSha: string, repo?: string): UseCommitDiffRe
} catch (err) {
if (requestSeq !== requestSeqRef.current) return;
toast({
title: "Failed to load commit diff",
description: err instanceof Error ? err.message : "An unexpected error occurred",
title: t("task:failedToLoadCommitDiff"),
description: err instanceof Error ? err.message : t("common:anUnexpectedErrorOccurred"),
variant: "error",
});
} finally {
@@ -66,7 +68,7 @@ export function useCommitDiff(commitSha: string, repo?: string): UseCommitDiffRe
setLoading(false);
}
}
}, [activeSessionId, activeTaskId, agentctlReady, commitSha, repo, sessionTaskId, toast]);
}, [activeSessionId, activeTaskId, agentctlReady, commitSha, repo, sessionTaskId, t, toast]);
useEffect(() => {
fetchDiff();
@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAppStoreApi } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { listPrompts } from "@/lib/api";
@@ -79,6 +80,7 @@ export function useRequestChangesWalkthrough({
}: UseRequestChangesWalkthroughParams) {
const storeApi = useAppStoreApi();
const { toast } = useToast();
const { t } = useTranslation("review");
return useCallback(async () => {
if (!taskId || !sessionId) return;
@@ -88,7 +90,7 @@ export function useRequestChangesWalkthrough({
const inputMode = deriveSessionInputMode(activeSession);
const planModeEnabled = state.chatInput.planModeBySessionId[sessionId] ?? false;
if (!ready) {
toast({ title: "Changes are still loading", variant: "error" });
toast({ title: t("review:changesStillLoading"), variant: "error" });
return;
}
if (inputMode === "unavailable") {
@@ -97,8 +99,8 @@ export function useRequestChangesWalkthrough({
// is nothing session-specific to say.
const title =
activeSession && TERMINAL_SESSION_STATES.has(activeSession.state)
? "Session has ended. Please create a new session to continue."
: "Session is not available for input";
? t("task:sessionEndedCreateNew")
: t("task:sessionUnavailableForInput");
toast({ title, variant: "error" });
return;
}
@@ -112,15 +114,15 @@ export function useRequestChangesWalkthrough({
content,
planModeEnabled,
});
toast({ title: "Walkthrough request queued", variant: "success" });
toast({ title: t("review:walkthroughRequestQueued"), variant: "success" });
return;
}
await sendWalkthroughRequest({ taskId, sessionId, content, planModeEnabled, state });
toast({ title: "Walkthrough request sent", variant: "success" });
toast({ title: t("review:walkthroughRequestSent"), variant: "success" });
} catch (error) {
console.error("Failed to request walkthrough:", error);
toast({ title: "Failed to request walkthrough", variant: "error" });
toast({ title: t("review:failedToRequestWalkthrough"), variant: "error" });
}
}, [ready, sessionId, storeApi, taskId, toast]);
}, [ready, sessionId, storeApi, t, taskId, toast]);
}
@@ -1,6 +1,7 @@
"use client";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { getWebSocketClient } from "@/lib/ws/connection";
@@ -39,29 +40,39 @@ type WsActionFn = (
function useWsAction(): WsActionFn {
const { toast, updateToast } = useToast();
const { t } = useTranslation("task");
return useCallback(
async (action, label, payload, timeout = 15000, feedback = "toast") => {
async (action, labelKey, payload, timeout = 15000, feedback = "toast") => {
const client = getWebSocketClient();
if (!client) return false;
const toastId =
feedback === "toast" ? toast({ title: `${label}...`, variant: "loading" }) : null;
feedback === "toast"
? toast({
title: t("task:sessionActionProgress", { action: t(labelKey) }),
variant: "loading",
})
: null;
try {
await client.request(action, payload, timeout);
if (toastId) {
updateToast(toastId, { title: `${label} successful`, variant: "success" });
updateToast(toastId, {
title: t("task:sessionActionSuccessful", { action: t(labelKey) }),
variant: "success",
});
}
return true;
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
const msg = error instanceof Error ? error.message : t("common:unknownError");
const title = t("task:sessionActionFailed", { action: t(labelKey) });
if (toastId) {
updateToast(toastId, { title: `${label} failed`, description: msg, variant: "error" });
updateToast(toastId, { title, description: msg, variant: "error" });
} else {
toast({ title: `${label} failed`, description: msg, variant: "error" });
toast({ title, description: msg, variant: "error" });
}
return false;
}
},
[toast, updateToast],
[t, toast, updateToast],
);
}
@@ -78,12 +89,19 @@ export function useSessionActions({ sessionId, taskId, onDeleted }: SessionActio
const setPrimary = useCallback(
() =>
sessionId &&
wsAction("session.set_primary", "Set primary", { session_id: sessionId }, 15000, "inline"),
wsAction(
"session.set_primary",
"task:sessionActionSetPrimary",
{ session_id: sessionId },
15000,
"inline",
),
[sessionId, wsAction],
);
const stop = useCallback(
() => sessionId && wsAction("session.stop", "Stopping session", { session_id: sessionId }),
() =>
sessionId && wsAction("session.stop", "task:sessionActionStop", { session_id: sessionId }),
[sessionId, wsAction],
);
@@ -93,7 +111,7 @@ export function useSessionActions({ sessionId, taskId, onDeleted }: SessionActio
taskId &&
wsAction(
"session.launch",
"Resuming session",
"task:sessionActionResume",
{ task_id: taskId, intent: "resume", session_id: sessionId },
30000,
),
@@ -105,7 +123,7 @@ export function useSessionActions({ sessionId, taskId, onDeleted }: SessionActio
if (!sessionId || !taskId) return false;
const ok = await wsAction(
"session.delete",
"Deleting session",
"task:sessionActionDelete",
{ session_id: sessionId },
15000,
options.feedback,
+14 -13
View File
@@ -1,4 +1,5 @@
import { useEffect, useCallback, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import {
getTaskPlan,
@@ -20,6 +21,7 @@ const EMPTY_REVISIONS: readonly TaskPlanRevision[] = Object.freeze([]);
* @param options.visible - When true, refetches the plan (use for tab visibility)
*/
export function useTaskPlan(taskId: string | null, options?: { visible?: boolean }) {
const { t } = useTranslation("task");
const { visible = true } = options ?? {};
const prevVisibleRef = useRef(visible);
const plan = useAppStore((state) => (taskId ? state.taskPlans.byTaskId[taskId] : undefined));
@@ -52,13 +54,12 @@ export function useTaskPlan(taskId: string | null, options?: { visible?: boolean
markTaskPlanSeen(taskId);
} catch (err) {
console.error("Failed to fetch task plan:", err);
setError(err instanceof Error ? err.message : "Failed to fetch plan");
setError(err instanceof Error ? err.message : t("task:failedToFetchPlan"));
} finally {
setTaskPlanLoading(taskId, false);
}
}, [taskId, setTaskPlan, setTaskPlanLoading, markTaskPlanSeen]);
}, [taskId, setTaskPlan, setTaskPlanLoading, markTaskPlanSeen, t]);
// Fetch plan on mount or when taskId changes
useEffect(() => {
if (connectionStatus !== "connected") return;
if (taskId && !isLoaded && !isLoading) {
@@ -69,11 +70,10 @@ export function useTaskPlan(taskId: string | null, options?: { visible?: boolean
// Refetch when becoming visible (e.g., tab switch)
useEffect(() => {
const wasHidden = !prevVisibleRef.current;
const isNowVisible = visible;
prevVisibleRef.current = visible;
// Only refetch when transitioning from hidden to visible
if (wasHidden && isNowVisible && connectionStatus === "connected" && taskId) {
if (wasHidden && visible && connectionStatus === "connected" && taskId) {
fetchPlan();
}
}, [visible, connectionStatus, taskId, fetchPlan]);
@@ -97,13 +97,13 @@ export function useTaskPlan(taskId: string | null, options?: { visible?: boolean
return savedPlan;
} catch (err) {
console.error("Failed to save task plan:", err);
setError(err instanceof Error ? err.message : "Failed to save plan");
setError(err instanceof Error ? err.message : t("task:failedToSavePlan"));
return null;
} finally {
setTaskPlanSaving(taskId, false);
}
},
[taskId, plan, setTaskPlan, setTaskPlanSaving],
[taskId, plan, setTaskPlan, setTaskPlanSaving, t],
);
const removePlan = useCallback(async (): Promise<boolean> => {
@@ -117,12 +117,12 @@ export function useTaskPlan(taskId: string | null, options?: { visible?: boolean
return true;
} catch (err) {
console.error("Failed to delete task plan:", err);
setError(err instanceof Error ? err.message : "Failed to delete plan");
setError(err instanceof Error ? err.message : t("task:failedToDeletePlan"));
return false;
} finally {
setTaskPlanSaving(taskId, false);
}
}, [taskId, setTaskPlan, setTaskPlanSaving]);
}, [taskId, setTaskPlan, setTaskPlanSaving, t]);
const revisionsBundle = useTaskPlanRevisions(taskId, setTaskPlanSaving, setError);
@@ -148,6 +148,7 @@ function useTaskPlanRevisions(
setTaskPlanSaving: (taskId: string, saving: boolean) => void,
setError: (err: string | null) => void,
) {
const { t } = useTranslation("task");
const revisions = useAppStore((state) =>
taskId ? (state.taskPlans.revisionsByTaskId[taskId] ?? EMPTY_REVISIONS) : EMPTY_REVISIONS,
) as TaskPlanRevision[];
@@ -171,11 +172,11 @@ function useTaskPlanRevisions(
setPlanRevisions(taskId, list);
} catch (err) {
console.error("Failed to load plan revisions:", err);
setError(err instanceof Error ? err.message : "Failed to load revisions");
setError(err instanceof Error ? err.message : t("task:failedToLoadPlanRevisions"));
} finally {
setPlanRevisionsLoading(taskId, false);
}
}, [taskId, setPlanRevisions, setPlanRevisionsLoading, setError]);
}, [taskId, setPlanRevisions, setPlanRevisionsLoading, setError, t]);
// Load revisions once on mount — events may have fired before the WS connected.
useEffect(() => {
@@ -211,13 +212,13 @@ function useTaskPlanRevisions(
return await revertPlanRevision(taskId, revisionId, authorName);
} catch (err) {
console.error("Failed to revert plan:", err);
setError(err instanceof Error ? err.message : "Failed to revert plan");
setError(err instanceof Error ? err.message : t("task:failedToRevertPlan"));
return null;
} finally {
setTaskPlanSaving(taskId, false);
}
},
[taskId, setTaskPlanSaving, setError],
[taskId, setTaskPlanSaving, setError, t],
);
return {
+5 -4
View File
@@ -17,6 +17,7 @@ import { useSessionGitStatus } from "@/hooks/domains/session/use-session-git-sta
import { useSaveDeleteActions } from "./use-file-save-delete";
import { buildRepoScopedItemId, PREVIEW_FILE_EDITOR_ID } from "@/lib/state/dockview-panel-actions";
import { useOpenFileWorkspaceSync } from "./file-editors-sync";
import { t } from "@/lib/i18n";
import {
fetchFileEditorState,
getPreviewItemIdToRemoveOnReplace,
@@ -410,8 +411,8 @@ function useOpenFileAction({
setFileState(fileKey, state);
} catch (error) {
toast({
title: "Failed to open file",
description: error instanceof Error ? error.message : "Unknown error",
title: t("task:failedToOpenFile"),
description: error instanceof Error ? error.message : t("common:unknownError"),
variant: "error",
});
}
@@ -489,8 +490,8 @@ function useMarkdownPreviewAction({
setFileState(fileKey, { ...state, markdownPreview: true });
} catch (error) {
toast({
title: "Failed to open file",
description: error instanceof Error ? error.message : "Unknown error",
title: t("task:failedToOpenFile"),
description: error instanceof Error ? error.message : t("common:unknownError"),
variant: "error",
});
}
+5 -4
View File
@@ -21,6 +21,7 @@ import { downloadFileContent } from "./use-file-operations";
const SESSION_ID = "sess-1";
const NESTED_PATH = "src/foo/bar.ts";
const UNKNOWN_ERROR = "Unknown error";
const FAKE_CLIENT = {} as unknown as Parameters<typeof downloadFileContent>[0];
beforeEach(() => {
@@ -36,7 +37,7 @@ describe("downloadFileContent", () => {
is_binary: false,
});
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, NESTED_PATH);
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, NESTED_PATH, UNKNOWN_ERROR);
expect(result).toEqual({ ok: true });
expect(requestFileContentMock).toHaveBeenCalledWith(FAKE_CLIENT, SESSION_ID, NESTED_PATH);
@@ -54,7 +55,7 @@ describe("downloadFileContent", () => {
is_binary: true,
});
await downloadFileContent(FAKE_CLIENT, SESSION_ID, "assets/logo.png");
await downloadFileContent(FAKE_CLIENT, SESSION_ID, "assets/logo.png", UNKNOWN_ERROR);
expect(triggerFileDownloadMock).toHaveBeenCalledWith({
fileName: "assets/logo.png",
@@ -70,7 +71,7 @@ describe("downloadFileContent", () => {
error: "Permission denied",
});
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, "src/foo.ts");
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, "src/foo.ts", UNKNOWN_ERROR);
expect(result).toEqual({ ok: false, error: "Permission denied" });
expect(triggerFileDownloadMock).not.toHaveBeenCalled();
@@ -79,7 +80,7 @@ describe("downloadFileContent", () => {
it("returns {ok: false} when the request throws", async () => {
requestFileContentMock.mockRejectedValueOnce(new Error("boom"));
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, "src/foo.ts");
const result = await downloadFileContent(FAKE_CLIENT, SESSION_ID, "src/foo.ts", UNKNOWN_ERROR);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toContain("boom");
+29 -14
View File
@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { getWebSocketClient } from "@/lib/ws/connection";
import type { WebSocketClient } from "@/lib/ws/client";
import { createFile, deleteFile, renameFile, requestFileContent } from "@/lib/ws/workspace-files";
@@ -8,7 +9,6 @@ import { useToast } from "@/components/toast-provider";
type ToastFn = ReturnType<typeof useToast>["toast"];
const ERROR_VARIANT = "error" as const;
const UNKNOWN_ERROR = "An unknown error occurred";
type DownloadResult = { ok: true } | { ok: false; error?: string };
@@ -20,6 +20,7 @@ export async function downloadFileContent(
client: WebSocketClient,
sessionId: string,
path: string,
unknownError: string,
): Promise<DownloadResult> {
try {
const response = await requestFileContent(client, sessionId, path);
@@ -31,24 +32,25 @@ export async function downloadFileContent(
});
return { ok: true };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : UNKNOWN_ERROR };
return { ok: false, error: error instanceof Error ? error.message : unknownError };
}
}
async function runFileOp<T extends { success: boolean; error?: string }>(
op: () => Promise<T>,
title: string,
unknownError: string,
toast: ToastFn,
): Promise<boolean> {
try {
const response = await op();
if (!response.success) {
toast({ title, description: response.error || UNKNOWN_ERROR, variant: ERROR_VARIANT });
toast({ title, description: response.error || unknownError, variant: ERROR_VARIANT });
return false;
}
return true;
} catch (error) {
const description = error instanceof Error ? error.message : UNKNOWN_ERROR;
const description = error instanceof Error ? error.message : unknownError;
toast({ title, description, variant: ERROR_VARIANT });
return false;
}
@@ -56,23 +58,35 @@ async function runFileOp<T extends { success: boolean; error?: string }>(
export function useFileOperations(sessionId: string | null) {
const { toast } = useToast();
const { t } = useTranslation("task");
const unknownError = t("common:anUnknownErrorOccurred");
const handleCreateFile = useCallback(
async (path: string): Promise<boolean> => {
const client = getWebSocketClient();
if (!client || !sessionId) return false;
return runFileOp(() => createFile(client, sessionId, path), "Failed to create file", toast);
return runFileOp(
() => createFile(client, sessionId, path),
t("task:failedToCreateFile"),
unknownError,
toast,
);
},
[sessionId, toast],
[sessionId, t, toast, unknownError],
);
const handleDeleteFile = useCallback(
async (path: string): Promise<boolean> => {
const client = getWebSocketClient();
if (!client || !sessionId) return false;
return runFileOp(() => deleteFile(client, sessionId, path), "Failed to delete item", toast);
return runFileOp(
() => deleteFile(client, sessionId, path),
t("task:failedToDeleteItem"),
unknownError,
toast,
);
},
[sessionId, toast],
[sessionId, t, toast, unknownError],
);
const handleRenameFile = useCallback(
@@ -81,29 +95,30 @@ export function useFileOperations(sessionId: string | null) {
if (!client || !sessionId) return false;
return runFileOp(
() => renameFile(client, sessionId, oldPath, newPath),
"Failed to rename item",
t("task:failedToRenameItem"),
unknownError,
toast,
);
},
[sessionId, toast],
[sessionId, t, toast, unknownError],
);
const handleDownloadFile = useCallback(
async (path: string): Promise<boolean> => {
const client = getWebSocketClient();
if (!client || !sessionId) return false;
const result = await downloadFileContent(client, sessionId, path);
const result = await downloadFileContent(client, sessionId, path, unknownError);
if (!result.ok) {
toast({
title: "Failed to download file",
description: result.error || UNKNOWN_ERROR,
title: t("task:failedToDownloadFile"),
description: result.error || unknownError,
variant: ERROR_VARIANT,
});
return false;
}
return true;
},
[sessionId, toast],
[sessionId, t, toast, unknownError],
);
return {
+2 -1
View File
@@ -5,6 +5,7 @@ import { toast } from "@/lib/toast/sonner";
import { detachTask, updateTask } from "@/lib/api";
import { useAppStoreApi } from "@/components/state-provider";
import type { WorkflowSnapshotData } from "@/lib/state/slices/kanban/types";
import { t } from "@/lib/i18n";
type StoreApi = ReturnType<typeof useAppStoreApi>;
type SnapshotTask = WorkflowSnapshotData["tasks"][number];
@@ -127,7 +128,7 @@ export function useNestTask() {
}
} catch (error) {
rollbackParent(op);
toast.error(error instanceof Error ? error.message : "Failed to nest task");
toast.error(error instanceof Error ? error.message : t("task:failedToNestTask"));
}
},
[store],
+3 -2
View File
@@ -5,6 +5,7 @@ import { useRequest } from "@/lib/http/use-request";
import { useDockviewStore } from "@/lib/state/dockview-store";
import { openFileInVscode } from "@/lib/api/domains/vscode-api";
import { useToast } from "@/components/toast-provider";
import { t } from "@/lib/i18n";
type OpenEditorOptions = {
filePath?: string;
@@ -88,8 +89,8 @@ export function useOpenSessionInEditor(sessionId?: string | null) {
return await request.run(options);
} catch (error) {
toast({
title: "Failed to open editor",
description: error instanceof Error ? error.message : "Request failed",
title: t("editors:failedToOpenEditor"),
description: error instanceof Error ? error.message : t("common:requestFailed"),
variant: "error",
});
return null;
+10 -4
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
@@ -11,16 +12,21 @@ export function useSidebarViewsSync() {
const clearError = useAppStore((s) => s.clearSidebarSyncError);
const clearTaskPrefsError = useAppStore((s) => s.clearSidebarTaskPrefsSyncError);
const { toast } = useToast();
const { t } = useTranslation("sidebar");
useEffect(() => {
if (!syncError) return;
toast({ title: "Sidebar views", description: syncError, variant: "error" });
toast({ title: t("sidebar:views"), description: syncError, variant: "error" });
clearError();
}, [syncError, toast, clearError]);
}, [syncError, t, toast, clearError]);
useEffect(() => {
if (!taskPrefsSyncError) return;
toast({ title: "Sidebar task preferences", description: taskPrefsSyncError, variant: "error" });
toast({
title: t("sidebar:taskPreferences"),
description: taskPrefsSyncError,
variant: "error",
});
clearTaskPrefsError();
}, [taskPrefsSyncError, toast, clearTaskPrefsError]);
}, [taskPrefsSyncError, t, toast, clearTaskPrefsError]);
}
+14 -25
View File
@@ -1,31 +1,13 @@
"use client";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { bulkMoveSelectedTasks } from "@/lib/api";
import { useToast } from "@/components/toast-provider";
function errorMessage(error: unknown) {
return error instanceof Error ? error.message : "Failed to move task";
}
function movedTitle(movedCount: number, destination: "step" | "workflow") {
if (movedCount === 1) return `Moved task to ${destination}`;
return `Moved ${movedCount} tasks to ${destination}`;
}
function movedDescription(movedCount: number, destination: "step" | "workflow") {
if (destination === "step") {
return movedCount === 1
? "The task is now in the selected step."
: "The tasks are now in the selected step.";
}
return movedCount === 1
? "Switch to the destination workflow to see it."
: "Switch to the destination workflow to see them.";
}
export function useTaskWorkflowMove() {
const { toast } = useToast();
const { t } = useTranslation("task");
return useCallback(
async (
@@ -43,19 +25,26 @@ export function useTaskWorkflowMove() {
target_step_id: targetStepId,
});
toast({
title: movedTitle(result.moved_count, destination),
description: movedDescription(result.moved_count, destination),
title: t(destination === "step" ? "task:movedTasksToStep" : "task:movedTasksToWorkflow", {
count: result.moved_count,
}),
description: t(
destination === "step"
? "task:movedTasksStepDescription"
: "task:movedTasksWorkflowDescription",
{ count: result.moved_count },
),
variant: "success",
});
} catch (error) {
toast({
title: "Failed to move task",
description: errorMessage(error),
title: t("task:failedToMoveTask"),
description: error instanceof Error ? error.message : t("task:failedToMoveTask"),
variant: "error",
});
throw error;
}
},
[toast],
[t, toast],
);
}
@@ -59,11 +59,12 @@ describe("buildConfigurableShortcutEntries", () => {
},
});
const entries = buildConfigurableShortcutEntries([plugin]);
const entries = buildConfigurableShortcutEntries([plugin], (key) => `translated:${key}`);
const coreEntry = entries.find((e) => e.source === "core" && e.id === "SEARCH");
const pluginEntry = entries.find((e) => e.source === "plugin" && e.id === NAMESPACED_ID);
expect(coreEntry).toBeDefined();
expect(coreEntry?.label).toBe("translated:settings:shortcutCommandPanel");
expect(pluginEntry).toMatchObject({
source: "plugin",
id: NAMESPACED_ID,
+11 -4
View File
@@ -45,11 +45,15 @@ export type ShortcutEntry =
* than raw `PluginRecord[]`) can merge core + plugin entries without
* re-deriving the core list themselves.
*/
export function coreShortcutEntries(): ShortcutEntry[] {
type TranslateShortcutLabel = (key: string) => string;
export function coreShortcutEntries(
translate: TranslateShortcutLabel = (key) => key,
): ShortcutEntry[] {
return (Object.keys(CONFIGURABLE_SHORTCUTS) as ConfigurableShortcutId[]).map((id) => ({
source: "core",
id,
label: CONFIGURABLE_SHORTCUTS[id].label,
label: translate(CONFIGURABLE_SHORTCUTS[id].labelKey),
default: CONFIGURABLE_SHORTCUTS[id].default,
}));
}
@@ -89,8 +93,11 @@ function buildPluginEntries(plugins: PluginRecord[]): ShortcutEntry[] {
* by plugin (dynamic) entries — for rendering in Settings > Keyboard
* Shortcuts. Core behavior/order is unchanged from `CONFIGURABLE_SHORTCUTS`.
*/
export function buildConfigurableShortcutEntries(plugins: PluginRecord[]): ShortcutEntry[] {
return [...coreShortcutEntries(), ...buildPluginEntries(plugins)];
export function buildConfigurableShortcutEntries(
plugins: PluginRecord[],
translate?: TranslateShortcutLabel,
): ShortcutEntry[] {
return [...coreShortcutEntries(translate), ...buildPluginEntries(plugins)];
}
/** Builds only the plugin-sourced entries — used by callers that already have core entries. */
@@ -31,49 +31,26 @@ describe("CONFIGURABLE_SHORTCUTS", () => {
expect(ids).toHaveLength(16);
});
it("each entry has a label and default matching SHORTCUTS", () => {
expect(CONFIGURABLE_SHORTCUTS.CONTENT_SEARCH.label).toBe("Search Task Contents");
it("each entry has a catalog key and default matching SHORTCUTS", () => {
for (const entry of Object.values(CONFIGURABLE_SHORTCUTS)) {
expect(entry.labelKey).toMatch(/^settings:shortcut/);
}
expect(CONFIGURABLE_SHORTCUTS.CONTENT_SEARCH.default).toBe(SHORTCUTS.CONTENT_SEARCH);
expect(CONFIGURABLE_SHORTCUTS.BOTTOM_TERMINAL.label).toBe("Toggle Bottom Terminal");
expect(CONFIGURABLE_SHORTCUTS.BOTTOM_TERMINAL.default).toBe(SHORTCUTS.BOTTOM_TERMINAL);
expect(CONFIGURABLE_SHORTCUTS.TOGGLE_SIDEBAR.label).toBe("Toggle Sidebar");
expect(CONFIGURABLE_SHORTCUTS.TOGGLE_SIDEBAR.default).toBe(UNBOUND_SHORTCUT);
expect(CONFIGURABLE_SHORTCUTS.COMMAND_PANEL.label).toBe("Command Panel (Alt)");
expect(CONFIGURABLE_SHORTCUTS.COMMAND_PANEL.default).toBe(SHORTCUTS.COMMAND_PANEL);
expect(CONFIGURABLE_SHORTCUTS.NEW_TASK.label).toBe("New Task");
expect(CONFIGURABLE_SHORTCUTS.NEW_TASK.default).toBe(SHORTCUTS.NEW_TASK);
expect(CONFIGURABLE_SHORTCUTS.FOCUS_INPUT.label).toBe("Focus Chat Input");
expect(CONFIGURABLE_SHORTCUTS.FOCUS_INPUT.default).toBe(SHORTCUTS.FOCUS_INPUT);
expect(CONFIGURABLE_SHORTCUTS.FOCUS_PASSTHROUGH_INPUT.label).toBe("Focus CLI Chat Input");
expect(CONFIGURABLE_SHORTCUTS.FOCUS_PASSTHROUGH_INPUT.default).toBe(
SHORTCUTS.FOCUS_PASSTHROUGH_INPUT,
);
expect(CONFIGURABLE_SHORTCUTS.TOGGLE_PLAN_MODE.label).toBe("Toggle Plan Mode");
expect(CONFIGURABLE_SHORTCUTS.TOGGLE_PLAN_MODE.default).toBe(SHORTCUTS.TOGGLE_PLAN_MODE);
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER.label).toBe("Recent Task Switcher");
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER.default).toBe(SHORTCUTS.TASK_SWITCHER);
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER_REVERSE.label).toBe(
"Recent Task Switcher (Backward)",
);
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER_REVERSE.default).toBe(
SHORTCUTS.TASK_SWITCHER_REVERSE,
);
expect(CONFIGURABLE_SHORTCUTS.REVERSE_SEARCH.label).toBe("Reverse Chat Search");
expect(CONFIGURABLE_SHORTCUTS.REVERSE_SEARCH.default).toBe(SHORTCUTS.REVERSE_SEARCH);
expect(CONFIGURABLE_SHORTCUTS.OPEN_TASK_PR.label).toBe("Open Task Pull Request");
expect(CONFIGURABLE_SHORTCUTS.OPEN_TASK_PR.default).toBe(SHORTCUTS.OPEN_TASK_PR);
expect(CONFIGURABLE_SHORTCUTS.WORKSPACE_PICKER.label).toBe("Open Workspace Picker");
expect(CONFIGURABLE_SHORTCUTS.WORKSPACE_PICKER.default).toBe(SHORTCUTS.WORKSPACE_PICKER);
});
});
+38 -17
View File
@@ -36,30 +36,51 @@ export function isUnboundShortcut(shortcut: KeyboardShortcut | undefined | null)
export const CONFIGURABLE_SHORTCUTS: Record<
ConfigurableShortcutId,
{ label: string; default: KeyboardShortcut }
{ labelKey: string; default: KeyboardShortcut }
> = {
SEARCH: { label: "Command Panel", default: SHORTCUTS.SEARCH },
FILE_SEARCH: { label: "File Search", default: SHORTCUTS.FILE_SEARCH },
CONTENT_SEARCH: { label: "Search Task Contents", default: SHORTCUTS.CONTENT_SEARCH },
QUICK_CHAT: { label: "Quick Chat", default: SHORTCUTS.QUICK_CHAT },
BOTTOM_TERMINAL: { label: "Toggle Bottom Terminal", default: SHORTCUTS.BOTTOM_TERMINAL },
TOGGLE_SIDEBAR: { label: "Toggle Sidebar", default: UNBOUND_SHORTCUT },
COMMAND_PANEL: { label: "Command Panel (Alt)", default: SHORTCUTS.COMMAND_PANEL },
NEW_TASK: { label: "New Task", default: SHORTCUTS.NEW_TASK },
FOCUS_INPUT: { label: "Focus Chat Input", default: SHORTCUTS.FOCUS_INPUT },
SEARCH: { labelKey: "settings:shortcutCommandPanel", default: SHORTCUTS.SEARCH },
FILE_SEARCH: { labelKey: "settings:shortcutFileSearch", default: SHORTCUTS.FILE_SEARCH },
CONTENT_SEARCH: {
labelKey: "settings:shortcutTaskContentSearch",
default: SHORTCUTS.CONTENT_SEARCH,
},
QUICK_CHAT: { labelKey: "settings:shortcutQuickChat", default: SHORTCUTS.QUICK_CHAT },
BOTTOM_TERMINAL: {
labelKey: "settings:shortcutBottomTerminal",
default: SHORTCUTS.BOTTOM_TERMINAL,
},
TOGGLE_SIDEBAR: { labelKey: "settings:shortcutToggleSidebar", default: UNBOUND_SHORTCUT },
COMMAND_PANEL: { labelKey: "settings:shortcutCommandPanelAlt", default: SHORTCUTS.COMMAND_PANEL },
NEW_TASK: { labelKey: "settings:shortcutNewTask", default: SHORTCUTS.NEW_TASK },
FOCUS_INPUT: { labelKey: "settings:shortcutFocusChatInput", default: SHORTCUTS.FOCUS_INPUT },
FOCUS_PASSTHROUGH_INPUT: {
label: "Focus CLI Chat Input",
labelKey: "settings:shortcutFocusCliChatInput",
default: SHORTCUTS.FOCUS_PASSTHROUGH_INPUT,
},
TOGGLE_PLAN_MODE: { label: "Toggle Plan Mode", default: SHORTCUTS.TOGGLE_PLAN_MODE },
TASK_SWITCHER: { label: "Recent Task Switcher", default: SHORTCUTS.TASK_SWITCHER },
TOGGLE_PLAN_MODE: {
labelKey: "settings:shortcutTogglePlanMode",
default: SHORTCUTS.TOGGLE_PLAN_MODE,
},
TASK_SWITCHER: {
labelKey: "settings:shortcutRecentTaskSwitcher",
default: SHORTCUTS.TASK_SWITCHER,
},
TASK_SWITCHER_REVERSE: {
label: "Recent Task Switcher (Backward)",
labelKey: "settings:shortcutRecentTaskSwitcherBackward",
default: SHORTCUTS.TASK_SWITCHER_REVERSE,
},
REVERSE_SEARCH: { label: "Reverse Chat Search", default: SHORTCUTS.REVERSE_SEARCH },
OPEN_TASK_PR: { label: "Open Task Pull Request", default: SHORTCUTS.OPEN_TASK_PR },
WORKSPACE_PICKER: { label: "Open Workspace Picker", default: SHORTCUTS.WORKSPACE_PICKER },
REVERSE_SEARCH: {
labelKey: "settings:shortcutReverseChatSearch",
default: SHORTCUTS.REVERSE_SEARCH,
},
OPEN_TASK_PR: {
labelKey: "settings:shortcutOpenTaskPullRequest",
default: SHORTCUTS.OPEN_TASK_PR,
},
WORKSPACE_PICKER: {
labelKey: "settings:shortcutOpenWorkspacePicker",
default: SHORTCUTS.WORKSPACE_PICKER,
},
};
export function getShortcut(
+30 -1
View File
@@ -253,5 +253,34 @@
"workItemUnavailable": "Work item unavailable.",
"workItemWatches": "Work-item watches",
"workItemWatchesDescription": "Create tasks automatically from matching Azure DevOps work-item queries.",
"workItemWatchFieldsRequired": "Project, WIQL, repository, workflow, step, agent, and executor are required."
"workItemWatchFieldsRequired": "Project, WIQL, repository, workflow, step, agent, and executor are required.",
"prStatusAbandoned": "Abandoned",
"prStatusActive": "Active",
"prStatusChangesRequested": "Changes requested",
"prStatusCompleted": "Completed",
"prStatusDraft": "Draft",
"prStatusPolicyFailed": "Policy failed",
"prStatusPolicyRunning": "Policy running",
"prStatusReady": "Ready",
"prStatusWaitingForReview": "Waiting for review",
"pullRequestStatusAria": "Azure PR {{id}}: {{status}}{{suffix}}",
"defaultActionAddressFeedback": "Address feedback",
"defaultActionAddressFeedbackHint": "Apply review comments",
"defaultActionFixCi": "Fix CI",
"defaultActionFixCiHint": "Diagnose failing checks",
"defaultActionImplement": "Implement",
"defaultActionImplementHint": "Build and open a PR",
"defaultActionInvestigate": "Investigate",
"defaultActionInvestigateHint": "Find the root cause",
"defaultActionReproduce": "Reproduce",
"defaultActionReproduceHint": "Document repro steps",
"defaultActionReview": "Review",
"defaultActionReviewHint": "Read the diff, flag issues",
"defaultQueryActive": "Active",
"defaultQueryAssignedToMe": "Assigned to me",
"defaultQueryCompleted": "Completed",
"defaultQueryCreatedByMe": "Created by me",
"defaultQueryOpen": "Open",
"defaultQueryRecentlyUpdated": "Recently updated",
"defaultQueryReviewRequested": "Review requested"
}
+30 -1
View File
@@ -441,5 +441,34 @@
"you": "You",
"youCanUseTheNavigationOr": "You can use the navigation or reload to try again.",
"yourAgentImplementsItInThe": "Your agent implements it in the kandev repository, with tests",
"youVerifyAndTestTheChange": "You verify and test the change in a second kandev instance"
"youVerifyAndTestTheChange": "You verify and test the change in a second kandev instance",
"failedToRenderDiagram": "Failed to render diagram",
"demoAgentMessagesDescription": "Visual inspection of agent message normalization. Each card shows the rendered output alongside raw JSON.",
"demoAgentMessagesQa": "Agent Messages QA",
"demoErrorLoading": "Error loading {{mode}}",
"demoEventFile": "Event file:",
"demoEvents": "events",
"demoFailedToLoadFiles": "Failed to load files",
"demoFailedToLoadFixtures": "Failed to load fixtures",
"demoFailedToLoadMessages": "Failed to load messages",
"demoFileOption_one": "{{protocol}} - {{agent}} ({{count}} message)",
"demoFileOption_other": "{{protocol}} - {{agent}} ({{count}} messages)",
"demoFixtureFile": "Fixture file:",
"demoFixtures": "fixtures",
"demoMessageData": "Message data",
"demoMessageTypes": "Message Types Demo",
"demoMessageTypesDescription": "All message types rendered for styling and testing",
"demoNoFilesFound": "No files found",
"demoNoFixtures": "No fixtures found. Select a fixture file from the dropdown above.",
"demoNoNormalizedEvents": "No normalized event files found. Run an agent with KANDEV_DEBUG_AGENT_MESSAGES=1 to generate event logs.",
"demoNormalizedEvents": "Normalized Events",
"demoNormalizedPayload": "Normalized Payload",
"demoRawInput": "Raw Input",
"demoRenderedOutput": "Rendered output:",
"demoShowingFixtures_one": "Showing {{count}} of {{total}} fixture",
"demoShowingFixtures_other": "Showing {{count}} of {{total}} fixtures",
"demoShowingMessages_one": "Showing {{count}} of {{total}} message",
"demoShowingMessages_other": "Showing {{count}} of {{total}} messages",
"demoTestFixtures": "Test Fixtures",
"demoToolType": "Tool type:"
}
+2 -1
View File
@@ -85,5 +85,6 @@
"switchToUnifiedView": "Switch to unified view",
"toggleCode": "Toggle code",
"toggleWordWrap": "Toggle word wrap",
"underlineCmdU": "Underline (Cmd+U)"
"underlineCmdU": "Underline (Cmd+U)",
"failedToOpenEditor": "Failed to open editor"
}
+4 -1
View File
@@ -347,5 +347,8 @@
"mrChipAutoMerge": "Auto-merge",
"mrChipDrawerTitle": "Merge request !{{mriid}}",
"mrChipDrawerDescription": "Pipeline status, review status, and merge request actions.",
"mrChipDrawerClose": "Close merge request status"
"mrChipDrawerClose": "Close merge request status",
"gitlabRejectedAssociation": "GitLab rejected the association.",
"taskCreatedMrNotLinked": "Task created, but merge request was not linked",
"taskCreatedMrNotLinkedDescription": "Open the task and use Link GitLab merge request to retry. {{error}}"
}
+2 -1
View File
@@ -91,5 +91,6 @@
"workflow": "Workflow",
"workflowStepFallback": "workflow step {{stepId}}",
"columns": "Columns",
"columnsForWorkflow": "Columns for {{name}}"
"columnsForWorkflow": "Columns for {{name}}",
"needsReassignment": "Needs Reassignment"
}
+11 -1
View File
@@ -78,5 +78,15 @@
"walkMeThroughTheseReviewChanges": "Walk me through these review changes",
"walkthrough": "Walkthrough",
"walkthroughDiscarded": "Walkthrough discarded",
"wrapLongLines": "Wrap long lines"
"wrapLongLines": "Wrap long lines",
"changesDiscarded": "Changes discarded",
"changesStillLoading": "Changes are still loading",
"couldNotSendFinding": "Could not send the finding",
"couldNotUpdateFinding": "Could not update finding",
"discardFailed": "Discard failed",
"failedToRequestWalkthrough": "Failed to request walkthrough",
"findingQueuedForAgent": "Finding queued for the agent",
"findingSentToAgent": "Finding sent to the agent",
"walkthroughRequestQueued": "Walkthrough request queued",
"walkthroughRequestSent": "Walkthrough request sent"
}
+17 -1
View File
@@ -692,5 +692,21 @@
"visualEditorUnavailable": "Visual editor unavailable",
"vsCodeRemoteSsh": "VS Code Remote SSH",
"whenEnabledInstallYourProjectS": "When enabled, install your project's dependencies (e.g. <2>{{command}}</2> via repository setup scripts) to avoid missing type errors.",
"externalMcpToolListTaskSessions": "List every agent session on a task, newest first, so another tool can address one by ID."
"externalMcpToolListTaskSessions": "List every agent session on a task, newest first, so another tool can address one by ID.",
"shortcutBottomTerminal": "Toggle Bottom Terminal",
"shortcutCommandPanel": "Command Panel",
"shortcutCommandPanelAlt": "Command Panel (Alt)",
"shortcutFileSearch": "File Search",
"shortcutFocusChatInput": "Focus Chat Input",
"shortcutFocusCliChatInput": "Focus CLI Chat Input",
"shortcutNewTask": "New Task",
"shortcutOpenTaskPullRequest": "Open Task Pull Request",
"shortcutOpenWorkspacePicker": "Open Workspace Picker",
"shortcutQuickChat": "Quick Chat",
"shortcutRecentTaskSwitcher": "Recent Task Switcher",
"shortcutRecentTaskSwitcherBackward": "Recent Task Switcher (Backward)",
"shortcutReverseChatSearch": "Reverse Chat Search",
"shortcutTaskContentSearch": "Search Task Contents",
"shortcutTogglePlanMode": "Toggle Plan Mode",
"shortcutToggleSidebar": "Toggle Sidebar"
}
+3 -1
View File
@@ -106,5 +106,7 @@
"viewLabel": "View: {{name}}",
"whatsNew": "What's new",
"work": "Work",
"workspaceFallback": "Workspace"
"workspaceFallback": "Workspace",
"taskPreferences": "Sidebar task preferences",
"views": "Sidebar views"
}
+47 -1
View File
@@ -1516,5 +1516,51 @@
"autopilotDescription": "The agent works independently and asks the parent task only when a critical decision blocks progress. It then waits for the answer before continuing.",
"autopilotInfoLabel": "More information about autopilot",
"autopilotSidebarDescription": "Autopilot task",
"autopilotChatDescription": "Autopilot is enabled. The agent asks its parent only for critical decisions."
"autopilotChatDescription": "Autopilot is enabled. The agent asks its parent only for critical decisions.",
"duplicateRepository": "Duplicate repository",
"duplicateRepositoryDescription": "{{repository}} is added more than once. Remove the duplicate row.",
"repositoryStillBeingVerified": "Repository is still being verified",
"repositoryProviderVerificationFailed": "Kandev could not verify this provider URL. Use Retry on the repository row.",
"repositoryProviderVerificationPending": "Wait for provider inspection to finish before creating the task.",
"failedToCreateFile": "Failed to create file",
"failedToCreateSession": "Failed to create session",
"failedToCreateTask": "Failed to create task",
"failedToDeleteItem": "Failed to delete item",
"failedToDeletePlan": "Failed to delete plan",
"failedToDownloadFile": "Failed to download file",
"failedToFetchPlan": "Failed to fetch plan",
"failedToLoadPlanRevisions": "Failed to load revisions",
"failedToRenameItem": "Failed to rename item",
"failedToRevertPlan": "Failed to revert plan",
"failedToSavePlan": "Failed to save plan",
"failedToStartTaskPlanMode": "Failed to start task in plan mode",
"failedToUpdateTask": "Failed to update task",
"noPullOrMergeRequestLinked": "No pull request or merge request linked to this task",
"sessionEndedCreateNew": "Session has ended. Please create a new session to continue.",
"sessionUnavailableForInput": "Session is not available for input",
"taskQueued": "Task queued",
"taskQueuedWipLimit": "The workflow step is at its WIP limit; this task will start when capacity opens.",
"failedToCheckRepositoryStatus": "Failed to check local repository status",
"failedToDiscoverRepositories": "Failed to discover repositories",
"failedToLoadCommitDiff": "Failed to load commit diff",
"failedToNestTask": "Failed to nest task",
"failedToProceedToNextStep": "Failed to proceed to next step",
"failedToStartImplementingPlan": "Failed to start implementing the plan",
"failedToStartImplementationSession": "Failed to start implementation session",
"failedToMoveTask": "Failed to move task",
"movedTasksStepDescription_one": "The task is now in the selected step.",
"movedTasksStepDescription_other": "The tasks are now in the selected step.",
"movedTasksToStep_one": "Moved task to step",
"movedTasksToStep_other": "Moved {{count}} tasks to step",
"movedTasksToWorkflow_one": "Moved task to workflow",
"movedTasksToWorkflow_other": "Moved {{count}} tasks to workflow",
"movedTasksWorkflowDescription_one": "Switch to the destination workflow to see it.",
"movedTasksWorkflowDescription_other": "Switch to the destination workflow to see them.",
"sessionActionDelete": "Deleting session",
"sessionActionFailed": "{{action}} failed",
"sessionActionProgress": "{{action}}...",
"sessionActionResume": "Resuming session",
"sessionActionSetPrimary": "Set primary",
"sessionActionStop": "Stopping session",
"sessionActionSuccessful": "{{action}} successful"
}
+30 -1
View File
@@ -253,5 +253,34 @@
"workItemUnavailable": "Ŵōŕķ ĩţēḿ ũńàvàĩĺàƀĺē.",
"workItemWatches": "Ŵōŕķ-ĩţēḿ ŵàţćĥēś",
"workItemWatchesDescription": "Ćŕēàţē ţàśķś àũţōḿàţĩćàĺĺŷ ƒŕōḿ ḿàţćĥĩńĝ Àźũŕē ĎēvŌƥś ŵōŕķ-ĩţēḿ qũēŕĩēś.",
"workItemWatchFieldsRequired": "Ƥŕōĵēćţ, ŴĨQĹ, ŕēƥōśĩţōŕŷ, ŵōŕķƒĺōŵ, śţēƥ, àĝēńţ, àńď ēxēćũţōŕ àŕē ŕēqũĩŕēď."
"workItemWatchFieldsRequired": "Ƥŕōĵēćţ, ŴĨQĹ, ŕēƥōśĩţōŕŷ, ŵōŕķƒĺōŵ, śţēƥ, àĝēńţ, àńď ēxēćũţōŕ àŕē ŕēqũĩŕēď.",
"prStatusAbandoned": "Àƀàńďōńēď",
"prStatusActive": "Àćţĩvē",
"prStatusChangesRequested": "Ćĥàńĝēś ŕēqũēśţēď",
"prStatusCompleted": "Ćōḿƥĺēţēď",
"prStatusDraft": "Ďŕàƒţ",
"prStatusPolicyFailed": "Ƥōĺĩćŷ ƒàĩĺēď",
"prStatusPolicyRunning": "Ƥōĺĩćŷ ŕũńńĩńĝ",
"prStatusReady": "Ŕēàďŷ",
"prStatusWaitingForReview": "Ŵàĩţĩńĝ ƒōŕ ŕēvĩēŵ",
"pullRequestStatusAria": "Àźũŕē ƤŔ {{id}}: {{status}}{{suffix}}",
"defaultActionAddressFeedback": "Àďďŕēśś ƒēēďƀàćķ",
"defaultActionAddressFeedbackHint": "Àƥƥĺŷ ŕēvĩēŵ ćōḿḿēńţś",
"defaultActionFixCi": "Ƒĩx ĆĨ",
"defaultActionFixCiHint": "Ďĩàĝńōśē ƒàĩĺĩńĝ ćĥēćķś",
"defaultActionImplement": "Ĩḿƥĺēḿēńţ",
"defaultActionImplementHint": "Ɓũĩĺď àńď ōƥēń à ƤŔ",
"defaultActionInvestigate": "Ĩńvēśţĩĝàţē",
"defaultActionInvestigateHint": "Ƒĩńď ţĥē ŕōōţ ćàũśē",
"defaultActionReproduce": "Ŕēƥŕōďũćē",
"defaultActionReproduceHint": "Ďōćũḿēńţ ŕēƥŕō śţēƥś",
"defaultActionReview": "Ŕēvĩēŵ",
"defaultActionReviewHint": "Ŕēàď ţĥē ďĩƒƒ, ƒĺàĝ ĩśśũēś",
"defaultQueryActive": "Àćţĩvē",
"defaultQueryAssignedToMe": "Àśśĩĝńēď ţō ḿē",
"defaultQueryCompleted": "Ćōḿƥĺēţēď",
"defaultQueryCreatedByMe": "Ćŕēàţēď ƀŷ ḿē",
"defaultQueryOpen": "Ōƥēń",
"defaultQueryRecentlyUpdated": "Ŕēćēńţĺŷ ũƥďàţēď",
"defaultQueryReviewRequested": "Ŕēvĩēŵ ŕēqũēśţēď"
}
+29 -1
View File
@@ -441,5 +441,33 @@
"you": "Ŷōũ",
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń.",
"yourAgentImplementsItInThe": "Ŷōũŕ àĝēńţ ĩḿƥĺēḿēńţś ĩţ ĩń ţĥē ķàńďēv ŕēƥōśĩţōŕŷ, ŵĩţĥ ţēśţś",
"youVerifyAndTestTheChange": "Ŷōũ vēŕĩƒŷ àńď ţēśţ ţĥē ćĥàńĝē ĩń à śēćōńď ķàńďēv ĩńśţàńćē"
"youVerifyAndTestTheChange": "Ŷōũ vēŕĩƒŷ àńď ţēśţ ţĥē ćĥàńĝē ĩń à śēćōńď ķàńďēv ĩńśţàńćē",
"demoAgentMessagesDescription": "Vĩśũàĺ ĩńśƥēćţĩōń ōƒ àĝēńţ ḿēśśàĝē ńōŕḿàĺĩźàţĩōń. Ēàćĥ ćàŕď śĥōŵś ţĥē ŕēńďēŕēď ōũţƥũţ àĺōńĝśĩďē ŕàŵ ĴŚŌŃ.",
"demoAgentMessagesQa": "Àĝēńţ Ḿēśśàĝēś QÀ",
"demoErrorLoading": "Ēŕŕōŕ ĺōàďĩńĝ {{mode}}",
"demoEventFile": "Ēvēńţ ƒĩĺē:",
"demoEvents": "ēvēńţś",
"demoFailedToLoadFiles": "Ƒàĩĺēď ţō ĺōàď ƒĩĺēś",
"demoFailedToLoadFixtures": "Ƒàĩĺēď ţō ĺōàď ƒĩxţũŕēś",
"demoFailedToLoadMessages": "Ƒàĩĺēď ţō ĺōàď ḿēśśàĝēś",
"demoFileOption_one": "{{protocol}} - {{agent}} ({{count}} ḿēśśàĝē)",
"demoFileOption_other": "{{protocol}} - {{agent}} ({{count}} ḿēśśàĝēś)",
"demoFixtureFile": "Ƒĩxţũŕē ƒĩĺē:",
"demoFixtures": "ƒĩxţũŕēś",
"demoMessageData": "Ḿēśśàĝē ďàţà",
"demoMessageTypes": "Ḿēśśàĝē Ţŷƥēś Ďēḿō",
"demoMessageTypesDescription": "Àĺĺ ḿēśśàĝē ţŷƥēś ŕēńďēŕēď ƒōŕ śţŷĺĩńĝ àńď ţēśţĩńĝ",
"demoNoFilesFound": "Ńō ƒĩĺēś ƒōũńď",
"demoNoFixtures": "Ńō ƒĩxţũŕēś ƒōũńď. Śēĺēćţ à ƒĩxţũŕē ƒĩĺē ƒŕōḿ ţĥē ďŕōƥďōŵń àƀōvē.",
"demoNoNormalizedEvents": "Ńō ńōŕḿàĺĩźēď ēvēńţ ƒĩĺēś ƒōũńď. Ŕũń àń àĝēńţ ŵĩţĥ ĶÀŃĎĒV_ĎĒƁŨĜ_ÀĜĒŃŢ_ḾĒŚŚÀĜĒŚ=1 ţō ĝēńēŕàţē ēvēńţ ĺōĝś.",
"demoNormalizedEvents": "Ńōŕḿàĺĩźēď Ēvēńţś",
"demoNormalizedPayload": "Ńōŕḿàĺĩźēď Ƥàŷĺōàď",
"demoRawInput": "Ŕàŵ Ĩńƥũţ",
"demoRenderedOutput": "Ŕēńďēŕēď ōũţƥũţ:",
"demoShowingFixtures_one": "Śĥōŵĩńĝ {{count}} ōƒ {{total}} ƒĩxţũŕē",
"demoShowingFixtures_other": "Śĥōŵĩńĝ {{count}} ōƒ {{total}} ƒĩxţũŕēś",
"demoShowingMessages_one": "Śĥōŵĩńĝ {{count}} ōƒ {{total}} ḿēśśàĝē",
"demoShowingMessages_other": "Śĥōŵĩńĝ {{count}} ōƒ {{total}} ḿēśśàĝēś",
"demoTestFixtures": "Ţēśţ Ƒĩxţũŕēś",
"demoToolType": "Ţōōĺ ţŷƥē:"
}
+2 -1
View File
@@ -85,5 +85,6 @@
"switchToUnifiedView": "Śŵĩţćĥ ţō ũńĩƒĩēď vĩēŵ",
"toggleCode": "Ţōĝĝĺē ćōďē",
"toggleWordWrap": "Ţōĝĝĺē ŵōŕď ŵŕàƥ",
"underlineCmdU": "Ũńďēŕĺĩńē (Ćḿď+Ũ)"
"underlineCmdU": "Ũńďēŕĺĩńē (Ćḿď+Ũ)",
"failedToOpenEditor": "Ƒàĩĺēď ţō ōƥēń ēďĩţōŕ"
}
+1 -1
View File
@@ -491,7 +491,7 @@
"selectWorkspace": "Śēĺēćţ ŵōŕķśƥàćē",
"selectedRepositories": "Śēĺēćţēď ŕēƥōśĩţōŕĩēś",
"selectWorkspaceBeforeRequestingReview": "Śēĺēćţ à ŵōŕķśƥàćē ƀēƒōŕē ŕēqũēśţĩńĝ ŕēvĩēŵ.",
"setTheseExactUrlsCreateAClientSecret": "Śēţ ţĥēśē ēxàćţ ŨŔĹś, ćŕēàţē à ćĺĩēńţ śēćŕēţ àńď ŵēƀĥōōķ śēćŕēţ, àńď ďōŵńĺōàď à ƥŕĩvàţē ķēŷ. Ţĥĩś ōńē-ţĩḿē śēţũƥ ēxƥĩŕēś {{expiresAt}}.",
"setTheseExactUrlsCreateAClientSecret": "Àďď ţĥē ţŵō ũśēŕ àũţĥōŕĩźàţĩōń ćàĺĺƀàćķ ŨŔĹś ĩń ţĥē ōŕďēŕ śĥōŵń, śēţ ţĥē ŕēḿàĩńĩńĝ ēxàćţ vàĺũēś, ćŕēàţē à ćĺĩēńţ śēćŕēţ àńď ŵēƀĥōōķ śēćŕēţ, àńď ďōŵńĺōàď à ƥŕĩvàţē ķēŷ. Ţĥĩś ōńē-ţĩḿē śēţũƥ ēxƥĩŕēś {{expiresAt}}.",
"showGithubApiLimits": "Śĥōŵ ĜĩţĤũƀ ÀƤĨ ĺĩḿĩţś",
"showLess": "Śĥōŵ ĺēśś",
"showMore": "Śĥōŵ ḿōŕē",
+4 -1
View File
@@ -347,5 +347,8 @@
"mrChipAutoMerge": "Àũţō-ḿēŕĝē",
"mrChipDrawerTitle": "Ḿēŕĝē ŕēqũēśţ !{{mriid}}",
"mrChipDrawerDescription": "Ƥĩƥēĺĩńē śţàţũś, ŕēvĩēŵ śţàţũś, àńď ḿēŕĝē ŕēqũēśţ àćţĩōńś.",
"mrChipDrawerClose": "Ćĺōśē ḿēŕĝē ŕēqũēśţ śţàţũś"
"mrChipDrawerClose": "Ćĺōśē ḿēŕĝē ŕēqũēśţ śţàţũś",
"gitlabRejectedAssociation": "ĜĩţĹàƀ ŕēĵēćţēď ţĥē àśśōćĩàţĩōń.",
"taskCreatedMrNotLinked": "Ţàśķ ćŕēàţēď, ƀũţ ḿēŕĝē ŕēqũēśţ ŵàś ńōţ ĺĩńķēď",
"taskCreatedMrNotLinkedDescription": "Ōƥēń ţĥē ţàśķ àńď ũśē Ĺĩńķ ĜĩţĹàƀ ḿēŕĝē ŕēqũēśţ ţō ŕēţŕŷ. {{error}}"
}
@@ -53,9 +53,9 @@
"copyingConfigTo": "Ćōƥŷĩńĝ {{label}} ćōńƒĩĝ ţō {{targetName}}…",
"createAsDraft": "Ćŕēàţē àś ďŕàƒţ",
"createdSuccessfully": "{{longName}} ćŕēàţēď śũććēśśƒũĺĺŷ",
"createdLinkNeedsAttention": "{{shortName}} ćŕēàţēď; ţàśķ ĺĩñķ ñēēďś àţţēñţĩőñ",
"associationRetryViaTaskMenu": "{{message}}. Ũśē Ĺĩñķ ĩñ ţĥē ţàśķ mēñũ ţő ŕēţŕŷ.",
"taskAssociationCouldNotBeSaved": "Ţàśķ àśśőćĩàţĩőñ ćőũĺď ñőţ bē śàvēď.",
"createdLinkNeedsAttention": "{{shortName}} ćŕēàţēď; ţàśķ ĺĩńķ ńēēďś àţţēńţĩōń",
"associationRetryViaTaskMenu": "{{message}}. Ũśē Ĺĩńķ ĩń ţĥē ţàśķ ḿēńũ ţō ŕēţŕŷ.",
"taskAssociationCouldNotBeSaved": "Ţàśķ àśśōćĩàţĩōń ćōũĺď ńōţ ƀē śàvēď.",
"createFailed": "Ćŕēàţē {{shortName}} ƒàĩĺēď",
"createLongName": "Ćŕēàţē {{longName}}",
"createPr": "Ćŕēàţē ƤŔ",
@@ -187,6 +187,6 @@
"queryNamePlaceholder": "ē.ĝ. Ńēēďś ḿŷ ŕēvĩēŵ",
"saveQuery": "Śàvē qũēŕŷ",
"waitingForTheNextBackendHealth": "Ŵàĩţĩńĝ ƒōŕ ţĥē ńēxţ ƀàćķēńď ĥēàĺţĥ ćĥēćķ…",
"cursorPaginationSummary_one": "Ƥàĝē {{page}} · {{count}} ŕēśũļţ",
"cursorPaginationSummary_other": "Ƥàĝē {{page}} · {{count}} ŕēśũļţś"
"cursorPaginationSummary_one": "Ƥàĝē {{page}} · {{count}} ŕēśũĺţ",
"cursorPaginationSummary_other": "Ƥàĝē {{page}} · {{count}} ŕēśũĺţś"
}
+2 -1
View File
@@ -91,5 +91,6 @@
"workflow": "Ŵōŕķƒĺōŵ",
"workflowStepFallback": "ŵōŕķƒĺōŵ śţēƥ {{stepId}}",
"columns": "Ćōĺũḿńś",
"columnsForWorkflow": "Ćōĺũḿńś ƒōŕ {{name}}"
"columnsForWorkflow": "Ćōĺũḿńś ƒōŕ {{name}}",
"needsReassignment": "Ńēēďś Ŕēàśśĩĝńḿēńţ"
}
+11 -1
View File
@@ -78,5 +78,15 @@
"walkMeThroughTheseReviewChanges": "Ŵàĺķ ḿē ţĥŕōũĝĥ ţĥēśē ŕēvĩēŵ ćĥàńĝēś",
"walkthrough": "Ŵàĺķţĥŕōũĝĥ",
"walkthroughDiscarded": "Ŵàĺķţĥŕōũĝĥ ďĩśćàŕďēď",
"wrapLongLines": "Ŵŕàƥ ĺōńĝ ĺĩńēś"
"wrapLongLines": "Ŵŕàƥ ĺōńĝ ĺĩńēś",
"changesDiscarded": "Ćĥàńĝēś ďĩśćàŕďēď",
"changesStillLoading": "Ćĥàńĝēś àŕē śţĩĺĺ ĺōàďĩńĝ",
"couldNotSendFinding": "Ćōũĺď ńōţ śēńď ţĥē ƒĩńďĩńĝ",
"couldNotUpdateFinding": "Ćōũĺď ńōţ ũƥďàţē ƒĩńďĩńĝ",
"discardFailed": "Ďĩśćàŕď ƒàĩĺēď",
"failedToRequestWalkthrough": "Ƒàĩĺēď ţō ŕēqũēśţ ŵàĺķţĥŕōũĝĥ",
"findingQueuedForAgent": "Ƒĩńďĩńĝ qũēũēď ƒōŕ ţĥē àĝēńţ",
"findingSentToAgent": "Ƒĩńďĩńĝ śēńţ ţō ţĥē àĝēńţ",
"walkthroughRequestQueued": "Ŵàĺķţĥŕōũĝĥ ŕēqũēśţ qũēũēď",
"walkthroughRequestSent": "Ŵàĺķţĥŕōũĝĥ ŕēqũēśţ śēńţ"
}
+17 -1
View File
@@ -692,5 +692,21 @@
"visualEditorUnavailable": "Vĩśũàĺ ēďĩţōŕ ũńàvàĩĺàƀĺē",
"vsCodeRemoteSsh": "VŚ Ćōďē Ŕēḿōţē ŚŚĤ",
"whenEnabledInstallYourProjectS": "Ŵĥēń ēńàƀĺēď, ĩńśţàĺĺ ŷōũŕ ƥŕōĵēćţ'ś ďēƥēńďēńćĩēś (ē.ĝ. <2>{{command}}</2> vĩà ŕēƥōśĩţōŕŷ śēţũƥ śćŕĩƥţś) ţō àvōĩď ḿĩśśĩńĝ ţŷƥē ēŕŕōŕś.",
"externalMcpToolListTaskSessions": "Ĺĩśţ ēvēŕŷ àĝēńţ śēśśĩōń ōń à ţàśķ, ńēŵēśţ ƒĩŕśţ, śō àńōţĥēŕ ţōōĺ ćàń àďďŕēśś ōńē ƀŷ ĨĎ."
"externalMcpToolListTaskSessions": "Ĺĩśţ ēvēŕŷ àĝēńţ śēśśĩōń ōń à ţàśķ, ńēŵēśţ ƒĩŕśţ, śō àńōţĥēŕ ţōōĺ ćàń àďďŕēśś ōńē ƀŷ ĨĎ.",
"shortcutBottomTerminal": "Ţōĝĝĺē Ɓōţţōḿ Ţēŕḿĩńàĺ",
"shortcutCommandPanel": "Ćōḿḿàńď Ƥàńēĺ",
"shortcutCommandPanelAlt": "Ćōḿḿàńď Ƥàńēĺ (Àĺţ)",
"shortcutFileSearch": "Ƒĩĺē Śēàŕćĥ",
"shortcutFocusChatInput": "Ƒōćũś Ćĥàţ Ĩńƥũţ",
"shortcutFocusCliChatInput": "Ƒōćũś ĆĹĨ Ćĥàţ Ĩńƥũţ",
"shortcutNewTask": "Ńēŵ Ţàśķ",
"shortcutOpenTaskPullRequest": "Ōƥēń Ţàśķ Ƥũĺĺ Ŕēqũēśţ",
"shortcutOpenWorkspacePicker": "Ōƥēń Ŵōŕķśƥàćē Ƥĩćķēŕ",
"shortcutQuickChat": "Qũĩćķ Ćĥàţ",
"shortcutRecentTaskSwitcher": "Ŕēćēńţ Ţàśķ Śŵĩţćĥēŕ",
"shortcutRecentTaskSwitcherBackward": "Ŕēćēńţ Ţàśķ Śŵĩţćĥēŕ (Ɓàćķŵàŕď)",
"shortcutReverseChatSearch": "Ŕēvēŕśē Ćĥàţ Śēàŕćĥ",
"shortcutTaskContentSearch": "Śēàŕćĥ Ţàśķ Ćōńţēńţś",
"shortcutTogglePlanMode": "Ţōĝĝĺē Ƥĺàń Ḿōďē",
"shortcutToggleSidebar": "Ţōĝĝĺē Śĩďēƀàŕ"
}
+3 -1
View File
@@ -106,5 +106,7 @@
"viewLabel": "Vĩēŵ: {{name}}",
"whatsNew": "Ŵĥàţ'ś ńēŵ",
"work": "Ŵōŕķ",
"workspaceFallback": "Ŵōŕķśƥàćē"
"workspaceFallback": "Ŵōŕķśƥàćē",
"taskPreferences": "Śĩďēƀàŕ ţàśķ ƥŕēƒēŕēńćēś",
"views": "Śĩďēƀàŕ vĩēŵś"
}
+50 -5
View File
@@ -934,8 +934,8 @@
"prompt": "Ƥŕōḿƥţ",
"promptFieldLabel": "ƥŕōḿƥţ",
"providerPrivateContextThatKandevDid": "Ƥŕōvĩďēŕ-ƥŕĩvàţē ćōńţēxţ ţĥàţ Ķàńďēv ďĩď ńōţ ŕēćōŕď ḿàŷ ńōţ ćàŕŕŷ ōvēŕ. Ōƥēń ţēŕḿĩńàĺś, ďēv śēŕvēŕś, àńď ōţĥēŕ ŵōŕķśƥàćē ƥŕōćēśśēś śţōƥ, ĩńćĺũďĩńĝ ţĥē ţàśķ ēďĩţōŕ śēŕvēŕ.",
"providerHistoryUnavailable": "Ćũŕŕēńţ ƤŔ ĥĩśţōŕŷ ĩś ũńàṽàĩĺàƀĺē. Ťĥē ćĥēćķōũţ ĥĩśţōŕŷ ĩś śĥōŵń ŵĩţĥōũţ àśśũḿĩńĝ à ŕēŵŕĩţē.",
"providerUnavailable": "Ƥŕōvĩďēŕ ũńàvàĩĺàƀĺē",
"providerHistoryUnavailable": "Ćũŕŕēńţ ƤŔ ĥĩśţōŕŷ ĩś ũńàvàĩĺàƀĺē. Ţĥē ćĥēćķōũţ ĥĩśţōŕŷ ĩś śĥōŵń ŵĩţĥōũţ àśśũḿĩńĝ à ŕēŵŕĩţē.",
"proxy": "Ƥŕōxŷ",
"publishARedactedSnapshotOfThis": "Ƥũƀĺĩśĥ à ŕēďàćţēď śńàƥśĥōţ ōƒ ţĥĩś ćōḿƥĺēţēď ţàśķ àś à śēćŕēţ ĜĩţĤũƀ Ĝĩśţ ōń ŷōũŕ àććōũńţ.",
"publishToGithubGist": "Ƥũƀĺĩśĥ ţō ĜĩţĤũƀ Ĝĩśţ",
@@ -992,9 +992,9 @@
"replacePRBranch": "Ŕēƥĺàćē ƤŔ ƀŕàńćĥ",
"usePRVersion": "Ũśē ƤŔ vēŕśĩōń",
"prNumberVersion": "ƤŔ #{{number}} vēŕśĩōń",
"remoteContributionReplaceDescription": "Ŕēƥĺàćē ţĥē ƥũƀĺĩśĥēď ƤŔ ƀŕàńćĥ ŵĩţĥ ŷōũŕ ũńćĥàńĝēď ţàśķ vēŕśĩōń. Ŕēƥũĩŕēś ćōńƒĩŕḿàţĩōń àńď àń ēxàćţ ƥŕōvĩďēŕ-ĥēàď ḿàţćĥ.",
"remoteContributionUseDescription": "Ũśē ţĥē ćũŕŕēńţ ƤŔ vēŕśĩōń ĩń ţĥĩś ţàśķ. Ķàńďēv ćŕēàţēś à ŕēćōvēŕŷ ƀŕàńćĥ ƒĩŕśţ àńď ŕēƥũĩŕēś à ćĺēàń ŵōŕķĩńĝ ţŕēē.",
"remoteContributionViewDescription": "Őƥēń ƤŔ #{{number}} vēŕśĩōń ŵĩţĥōũţ ćĥàńĝĩńĝ ţĥē ţàśķ ćĥēćķōũţ.",
"remoteContributionReplaceDescription": "Ŕēƥĺàćē ţĥē ƥũƀĺĩśĥēď ƤŔ ƀŕàńćĥ ŵĩţĥ ŷōũŕ ũńćĥàńĝēď ţàśķ vēŕśĩōń. Ŕēqũĩŕēś ćōńƒĩŕḿàţĩōń àńď àń ēxàćţ ƥŕōvĩďēŕ-ĥēàď ḿàţćĥ.",
"remoteContributionUseDescription": "Ũśē ţĥē ćũŕŕēńţ ƤŔ vēŕśĩōń ĩń ţĥĩś ţàśķ. Ķàńďēv ćŕēàţēś à ŕēćōvēŕŷ ƀŕàńćĥ ƒĩŕśţ àńď ŕēqũĩŕēś à ćĺēàń ŵōŕķĩńĝ ţŕēē.",
"remoteContributionViewDescription": "Ōƥēń ƤŔ #{{number}} vēŕśĩōń ŵĩţĥōũţ ćĥàńĝĩńĝ ţĥē ţàśķ ćĥēćķōũţ.",
"replacePRBranchConfirmation": "Ŕēƥĺàćē ţĥē ƥũƀĺĩśĥēď ƤŔ ƀŕàńćĥ ƒōŕ {{repository}} àţ {{head}} ŵĩţĥ ţĥē ţàśķ vēŕśĩōń? Ţĥĩś ćĥàńĝēś ţĥē ƥŕōvĩďēŕ ƀŕàńćĥ ţō ḿàţćĥ ţĥē ţàśķ vēŕśĩōń.",
"usePRVersionConfirmation": "Ũśē ţĥē ƤŔ vēŕśĩōń àţ {{head}}? Ķàńďēv ŵĩĺĺ ćŕēàţē à ŕēćōvēŕŷ ƀŕàńćĥ ƒōŕ ţĥē ćũŕŕēńţ ţàśķ vēŕśĩōń ƒĩŕśţ. Ţĥē ŵōŕķĩńĝ ţŕēē ḿũśţ ƀē ćĺēàń.",
"remoteContributionProviderChangedAgain": "Ţĥē ƤŔ ćĥàńĝēď àĝàĩń. Ŕēƒŕēśĥ ţĥē ƥŕōvĩďēŕ vēŕśĩōń àńď ţŕŷ àĝàĩń.",
@@ -1516,5 +1516,50 @@
"autopilotDescription": "Ţĥē àĝēńţ ŵōŕķś ĩńďēƥēńďēńţĺŷ àńď àśķś ţĥē ƥàŕēńţ ţàśķ ōńĺŷ ŵĥēń à ćŕĩţĩćàĺ ďēćĩśĩōń ƀĺōćķś ƥŕōĝŕēśś. Ĩţ ţĥēń ŵàĩţś ƒōŕ ţĥē àńśŵēŕ ƀēƒōŕē ćōńţĩńũĩńĝ.",
"autopilotInfoLabel": "Ḿōŕē ĩńƒōŕḿàţĩōń àƀōũţ àũţōƥĩĺōţ",
"autopilotSidebarDescription": "Àũţōƥĩĺōţ ţàśķ",
"autopilotChatDescription": "Àũţōƥĩĺōţ ĩś ēńàƀĺēď. Ţĥē àĝēńţ àśķś ĩţś ƥàŕēńţ ōńĺŷ ƒōŕ ćŕĩţĩćàĺ ďēćĩśĩōńś."
"autopilotChatDescription": "Àũţōƥĩĺōţ ĩś ēńàƀĺēď. Ţĥē àĝēńţ àśķś ĩţś ƥàŕēńţ ōńĺŷ ƒōŕ ćŕĩţĩćàĺ ďēćĩśĩōńś.",
"duplicateRepository": "Ďũƥĺĩćàţē ŕēƥōśĩţōŕŷ",
"duplicateRepositoryDescription": "{{repository}} ĩś àďďēď ḿōŕē ţĥàń ōńćē. Ŕēḿōvē ţĥē ďũƥĺĩćàţē ŕōŵ.",
"repositoryStillBeingVerified": "Ŕēƥōśĩţōŕŷ ĩś śţĩĺĺ ƀēĩńĝ vēŕĩƒĩēď",
"repositoryProviderVerificationFailed": "Ķàńďēv ćōũĺď ńōţ vēŕĩƒŷ ţĥĩś ƥŕōvĩďēŕ ŨŔĹ. Ũśē Ŕēţŕŷ ōń ţĥē ŕēƥōśĩţōŕŷ ŕōŵ.",
"repositoryProviderVerificationPending": "Ŵàĩţ ƒōŕ ƥŕōvĩďēŕ ĩńśƥēćţĩōń ţō ƒĩńĩśĥ ƀēƒōŕē ćŕēàţĩńĝ ţĥē ţàśķ.",
"failedToCreateFile": "Ƒàĩĺēď ţō ćŕēàţē ƒĩĺē",
"failedToCreateSession": "Ƒàĩĺēď ţō ćŕēàţē śēśśĩōń",
"failedToCreateTask": "Ƒàĩĺēď ţō ćŕēàţē ţàśķ",
"failedToDeleteItem": "Ƒàĩĺēď ţō ďēĺēţē ĩţēḿ",
"failedToDeletePlan": "Ƒàĩĺēď ţō ďēĺēţē ƥĺàń",
"failedToDownloadFile": "Ƒàĩĺēď ţō ďōŵńĺōàď ƒĩĺē",
"failedToFetchPlan": "Ƒàĩĺēď ţō ƒēţćĥ ƥĺàń",
"failedToLoadPlanRevisions": "Ƒàĩĺēď ţō ĺōàď ŕēvĩśĩōńś",
"failedToRevertPlan": "Ƒàĩĺēď ţō ŕēvēŕţ ƥĺàń",
"failedToSavePlan": "Ƒàĩĺēď ţō śàvē ƥĺàń",
"failedToStartTaskPlanMode": "Ƒàĩĺēď ţō śţàŕţ ţàśķ ĩń ƥĺàń ḿōďē",
"failedToUpdateTask": "Ƒàĩĺēď ţō ũƥďàţē ţàśķ",
"noPullOrMergeRequestLinked": "Ńō ƥũĺĺ ŕēqũēśţ ōŕ ḿēŕĝē ŕēqũēśţ ĺĩńķēď ţō ţĥĩś ţàśķ",
"sessionEndedCreateNew": "Śēśśĩōń ĥàś ēńďēď. Ƥĺēàśē ćŕēàţē à ńēŵ śēśśĩōń ţō ćōńţĩńũē.",
"sessionUnavailableForInput": "Śēśśĩōń ĩś ńōţ àvàĩĺàƀĺē ƒōŕ ĩńƥũţ",
"taskQueued": "Ţàśķ qũēũēď",
"taskQueuedWipLimit": "Ţĥē ŵōŕķƒĺōŵ śţēƥ ĩś àţ ĩţś ŴĨƤ ĺĩḿĩţ; ţĥĩś ţàśķ ŵĩĺĺ śţàŕţ ŵĥēń ćàƥàćĩţŷ ōƥēńś.",
"failedToCheckRepositoryStatus": "Ƒàĩĺēď ţō ćĥēćķ ĺōćàĺ ŕēƥōśĩţōŕŷ śţàţũś",
"failedToDiscoverRepositories": "Ƒàĩĺēď ţō ďĩśćōvēŕ ŕēƥōśĩţōŕĩēś",
"failedToLoadCommitDiff": "Ƒàĩĺēď ţō ĺōàď ćōḿḿĩţ ďĩƒƒ",
"failedToNestTask": "Ƒàĩĺēď ţō ńēśţ ţàśķ",
"failedToProceedToNextStep": "Ƒàĩĺēď ţō ƥŕōćēēď ţō ńēxţ śţēƥ",
"failedToStartImplementingPlan": "Ƒàĩĺēď ţō śţàŕţ ĩḿƥĺēḿēńţĩńĝ ţĥē ƥĺàń",
"failedToStartImplementationSession": "Ƒàĩĺēď ţō śţàŕţ ĩḿƥĺēḿēńţàţĩōń śēśśĩōń",
"failedToMoveTask": "Ƒàĩĺēď ţō ḿōvē ţàśķ",
"movedTasksStepDescription_one": "Ţĥē ţàśķ ĩś ńōŵ ĩń ţĥē śēĺēćţēď śţēƥ.",
"movedTasksStepDescription_other": "Ţĥē ţàśķś àŕē ńōŵ ĩń ţĥē śēĺēćţēď śţēƥ.",
"movedTasksToStep_one": "Ḿōvēď ţàśķ ţō śţēƥ",
"movedTasksToStep_other": "Ḿōvēď {{count}} ţàśķś ţō śţēƥ",
"movedTasksToWorkflow_one": "Ḿōvēď ţàśķ ţō ŵōŕķƒĺōŵ",
"movedTasksToWorkflow_other": "Ḿōvēď {{count}} ţàśķś ţō ŵōŕķƒĺōŵ",
"movedTasksWorkflowDescription_one": "Śŵĩţćĥ ţō ţĥē ďēśţĩńàţĩōń ŵōŕķƒĺōŵ ţō śēē ĩţ.",
"movedTasksWorkflowDescription_other": "Śŵĩţćĥ ţō ţĥē ďēśţĩńàţĩōń ŵōŕķƒĺōŵ ţō śēē ţĥēḿ.",
"sessionActionDelete": "Ďēĺēţĩńĝ śēśśĩōń",
"sessionActionFailed": "{{action}} ƒàĩĺēď",
"sessionActionProgress": "{{action}}...",
"sessionActionResume": "Ŕēśũḿĩńĝ śēśśĩōń",
"sessionActionSetPrimary": "Śēţ ƥŕĩḿàŕŷ",
"sessionActionStop": "Śţōƥƥĩńĝ śēśśĩōń",
"sessionActionSuccessful": "{{action}} śũććēśśƒũĺ"
}
+30 -1
View File
@@ -253,5 +253,34 @@
"workItemUnavailable": "Item de trabalho indisponível.",
"workItemWatches": "Vigilâncias de itens de trabalho",
"workItemWatchesDescription": "Crie tarefas automaticamente a partir das consultas de itens de trabalho correspondentes do Azure DevOps.",
"workItemWatchFieldsRequired": "O projeto, o WIQL, o repositório, o fluxo de trabalho, o passo, o agente e o executor são obrigatórios."
"workItemWatchFieldsRequired": "O projeto, o WIQL, o repositório, o fluxo de trabalho, o passo, o agente e o executor são obrigatórios.",
"prStatusAbandoned": "Abandonado",
"prStatusActive": "Ativo",
"prStatusChangesRequested": "Alterações solicitadas",
"prStatusCompleted": "Concluído",
"prStatusDraft": "Rascunho",
"prStatusPolicyFailed": "Política falhou",
"prStatusPolicyRunning": "Política em execução",
"prStatusReady": "Pronto",
"prStatusWaitingForReview": "A aguardar revisão",
"pullRequestStatusAria": "PR Azure {{id}}: {{status}}{{suffix}}",
"defaultActionAddressFeedback": "Responder ao feedback",
"defaultActionAddressFeedbackHint": "Aplicar comentários da revisão",
"defaultActionFixCi": "Corrigir CI",
"defaultActionFixCiHint": "Diagnosticar verificações com falha",
"defaultActionImplement": "Implementar",
"defaultActionImplementHint": "Desenvolver e abrir um PR",
"defaultActionInvestigate": "Investigar",
"defaultActionInvestigateHint": "Encontrar a causa raiz",
"defaultActionReproduce": "Reproduzir",
"defaultActionReproduceHint": "Documentar passos de reprodução",
"defaultActionReview": "Rever",
"defaultActionReviewHint": "Ler as diferenças e assinalar problemas",
"defaultQueryActive": "Ativos",
"defaultQueryAssignedToMe": "Atribuídos a mim",
"defaultQueryCompleted": "Concluídos",
"defaultQueryCreatedByMe": "Criados por mim",
"defaultQueryOpen": "Abertos",
"defaultQueryRecentlyUpdated": "Atualizados recentemente",
"defaultQueryReviewRequested": "Revisão solicitada"
}
+30 -1
View File
@@ -431,5 +431,34 @@
"you": "Você",
"youCanUseTheNavigationOr": "Pode usar a navegação ou recarregar para tentar novamente.",
"yourAgentImplementsItInThe": "O seu agente implementa-a no repositório do kandev, com testes",
"youVerifyAndTestTheChange": "Verifica e testa a alteração numa segunda instância do kandev"
"youVerifyAndTestTheChange": "Verifica e testa a alteração numa segunda instância do kandev",
"failedToRenderDiagram": "Falha ao renderizar o diagrama",
"demoAgentMessagesDescription": "Inspeção visual da normalização de mensagens do agente. Cada cartão mostra o resultado renderizado junto ao JSON original.",
"demoAgentMessagesQa": "QA de mensagens do agente",
"demoErrorLoading": "Erro ao carregar {{mode}}",
"demoEventFile": "Ficheiro de eventos:",
"demoEvents": "eventos",
"demoFailedToLoadFiles": "Falha ao carregar ficheiros",
"demoFailedToLoadFixtures": "Falha ao carregar dados de teste",
"demoFailedToLoadMessages": "Falha ao carregar mensagens",
"demoFileOption_one": "{{protocol}} - {{agent}} ({{count}} mensagem)",
"demoFileOption_other": "{{protocol}} - {{agent}} ({{count}} mensagens)",
"demoFixtureFile": "Ficheiro de teste:",
"demoFixtures": "dados de teste",
"demoMessageData": "Dados da mensagem",
"demoMessageTypes": "Demonstração de tipos de mensagem",
"demoMessageTypesDescription": "Todos os tipos de mensagem renderizados para estilo e testes",
"demoNoFilesFound": "Nenhum ficheiro encontrado",
"demoNoFixtures": "Nenhum dado de teste encontrado. Selecione um ficheiro na lista acima.",
"demoNoNormalizedEvents": "Nenhum ficheiro de eventos normalizados encontrado. Execute um agente com KANDEV_DEBUG_AGENT_MESSAGES=1 para gerar registos de eventos.",
"demoNormalizedEvents": "Eventos normalizados",
"demoNormalizedPayload": "Conteúdo normalizado",
"demoRawInput": "Entrada original",
"demoRenderedOutput": "Resultado renderizado:",
"demoShowingFixtures_one": "A mostrar {{count}} de {{total}} dado de teste",
"demoShowingFixtures_other": "A mostrar {{count}} de {{total}} dados de teste",
"demoShowingMessages_one": "A mostrar {{count}} de {{total}} mensagem",
"demoShowingMessages_other": "A mostrar {{count}} de {{total}} mensagens",
"demoTestFixtures": "Dados de teste",
"demoToolType": "Tipo de ferramenta:"
}
+2 -1
View File
@@ -85,5 +85,6 @@
"switchToUnifiedView": "Mudar para a vista unificada",
"toggleCode": "Alternar código",
"toggleWordWrap": "Alternar a quebra de linha",
"underlineCmdU": "Sublinhado (Cmd+U)"
"underlineCmdU": "Sublinhado (Cmd+U)",
"failedToOpenEditor": "Falha ao abrir o editor"
}
+4 -1
View File
@@ -331,5 +331,8 @@
"mrTooltipState": "Estado: {{state}}",
"mrTooltipReview": "Revisão: {{state}}",
"mrTooltipPipeline": "Pipeline: {{state}}",
"mrTooltipReadyToMerge": "Pronto para merge"
"mrTooltipReadyToMerge": "Pronto para merge",
"gitlabRejectedAssociation": "O GitLab rejeitou a associação.",
"taskCreatedMrNotLinked": "Tarefa criada, mas o merge request não foi associado",
"taskCreatedMrNotLinkedDescription": "Abra a tarefa e use Associar merge request do GitLab para tentar novamente. {{error}}"
}
+2 -1
View File
@@ -89,5 +89,6 @@
"whenEnabledClickingATaskOpens": "Quando ativado, clicar numa tarefa abre o painel de pré-visualização. Quando desativado, clicar navega diretamente para a sessão.",
"wipTaskCount": "Tarefas em {{label}}",
"workflow": "Fluxo de trabalho",
"workflowStepFallback": "passo do fluxo de trabalho {{stepId}}"
"workflowStepFallback": "passo do fluxo de trabalho {{stepId}}",
"needsReassignment": "Precisa de reatribuição"
}
+11 -1
View File
@@ -78,5 +78,15 @@
"walkMeThroughTheseReviewChanges": "Explicar passo a passo estas alterações da revisão",
"walkthrough": "Passo a passo",
"walkthroughDiscarded": "Passo a passo descartado",
"wrapLongLines": "Quebrar as linhas longas"
"wrapLongLines": "Quebrar as linhas longas",
"changesDiscarded": "Alterações descartadas",
"changesStillLoading": "As alterações ainda estão a carregar",
"couldNotSendFinding": "Não foi possível enviar a constatação",
"couldNotUpdateFinding": "Não foi possível atualizar a constatação",
"discardFailed": "Falha ao descartar",
"failedToRequestWalkthrough": "Falha ao solicitar o passo a passo",
"findingQueuedForAgent": "Constatação adicionada à fila do agente",
"findingSentToAgent": "Constatação enviada ao agente",
"walkthroughRequestQueued": "Pedido de passo a passo em fila",
"walkthroughRequestSent": "Pedido de passo a passo enviado"
}
+17 -1
View File
@@ -676,5 +676,21 @@
"viewAvailableSettings": "Ver as definições disponíveis",
"visualEditorUnavailable": "Editor visual indisponível",
"vsCodeRemoteSsh": "VS Code Remote SSH",
"whenEnabledInstallYourProjectS": "Quando está ativado, instale as dependências do seu projeto (por exemplo, <2>{{command}}</2> através dos scripts de configuração do repositório) para evitar erros de tipos por falta de dependências."
"whenEnabledInstallYourProjectS": "Quando está ativado, instale as dependências do seu projeto (por exemplo, <2>{{command}}</2> através dos scripts de configuração do repositório) para evitar erros de tipos por falta de dependências.",
"shortcutBottomTerminal": "Alternar terminal inferior",
"shortcutCommandPanel": "Painel de comandos",
"shortcutCommandPanelAlt": "Painel de comandos (alternativo)",
"shortcutFileSearch": "Pesquisa de ficheiros",
"shortcutFocusChatInput": "Focar entrada do chat",
"shortcutFocusCliChatInput": "Focar entrada do chat CLI",
"shortcutNewTask": "Nova tarefa",
"shortcutOpenTaskPullRequest": "Abrir pull request da tarefa",
"shortcutOpenWorkspacePicker": "Abrir seletor de áreas de trabalho",
"shortcutQuickChat": "Chat rápido",
"shortcutRecentTaskSwitcher": "Seletor de tarefas recentes",
"shortcutRecentTaskSwitcherBackward": "Seletor de tarefas recentes (para trás)",
"shortcutReverseChatSearch": "Pesquisa inversa no chat",
"shortcutTaskContentSearch": "Pesquisar conteúdo da tarefa",
"shortcutTogglePlanMode": "Alternar modo de plano",
"shortcutToggleSidebar": "Alternar barra lateral"
}
+3 -1
View File
@@ -104,5 +104,7 @@
"viewLabel": "Vista: {{name}}",
"whatsNew": "Novidades",
"work": "Trabalho",
"workspaceFallback": "Área de trabalho"
"workspaceFallback": "Área de trabalho",
"taskPreferences": "Preferências de tarefas da barra lateral",
"views": "Vistas da barra lateral"
}
+47 -1
View File
@@ -1516,5 +1516,51 @@
"autopilotDescription": "O agente trabalha de forma independente e pergunta à tarefa pai apenas quando uma decisão crítica bloqueia o progresso. Depois, espera pela resposta antes de continuar.",
"autopilotInfoLabel": "Mais informações sobre o piloto automático",
"autopilotSidebarDescription": "Tarefa em piloto automático",
"autopilotChatDescription": "O piloto automático está ativo. O agente pergunta ao seu pai apenas sobre decisões críticas."
"autopilotChatDescription": "O piloto automático está ativo. O agente pergunta ao seu pai apenas sobre decisões críticas.",
"duplicateRepository": "Repositório duplicado",
"duplicateRepositoryDescription": "{{repository}} foi adicionado mais de uma vez. Remova a linha duplicada.",
"repositoryStillBeingVerified": "O repositório ainda está a ser verificado",
"repositoryProviderVerificationFailed": "O Kandev não conseguiu verificar este URL do fornecedor. Use Repetir na linha do repositório.",
"repositoryProviderVerificationPending": "Aguarde que a inspeção do fornecedor termine antes de criar a tarefa.",
"failedToCreateFile": "Falha ao criar o ficheiro",
"failedToCreateSession": "Falha ao criar a sessão",
"failedToCreateTask": "Falha ao criar a tarefa",
"failedToDeleteItem": "Falha ao eliminar o item",
"failedToDeletePlan": "Falha ao eliminar o plano",
"failedToDownloadFile": "Falha ao transferir o ficheiro",
"failedToFetchPlan": "Falha ao obter o plano",
"failedToLoadPlanRevisions": "Falha ao carregar revisões",
"failedToRenameItem": "Falha ao mudar o nome do item",
"failedToRevertPlan": "Falha ao reverter o plano",
"failedToSavePlan": "Falha ao guardar o plano",
"failedToStartTaskPlanMode": "Falha ao iniciar a tarefa no modo de plano",
"failedToUpdateTask": "Falha ao atualizar a tarefa",
"noPullOrMergeRequestLinked": "Nenhum pull request ou merge request associado a esta tarefa",
"sessionEndedCreateNew": "A sessão terminou. Crie uma nova sessão para continuar.",
"sessionUnavailableForInput": "A sessão não está disponível para entrada",
"taskQueued": "Tarefa em fila",
"taskQueuedWipLimit": "A etapa do fluxo de trabalho atingiu o limite WIP; esta tarefa será iniciada quando houver capacidade.",
"failedToCheckRepositoryStatus": "Falha ao verificar o estado do repositório local",
"failedToDiscoverRepositories": "Falha ao descobrir repositórios",
"failedToLoadCommitDiff": "Falha ao carregar as diferenças do commit",
"failedToNestTask": "Falha ao aninhar a tarefa",
"failedToProceedToNextStep": "Falha ao avançar para o passo seguinte",
"failedToStartImplementingPlan": "Falha ao iniciar a implementação do plano",
"failedToStartImplementationSession": "Falha ao iniciar a sessão de implementação",
"failedToMoveTask": "Falha ao mover a tarefa",
"movedTasksStepDescription_one": "A tarefa está agora no passo selecionado.",
"movedTasksStepDescription_other": "As tarefas estão agora no passo selecionado.",
"movedTasksToStep_one": "Tarefa movida para o passo",
"movedTasksToStep_other": "{{count}} tarefas movidas para o passo",
"movedTasksToWorkflow_one": "Tarefa movida para o fluxo de trabalho",
"movedTasksToWorkflow_other": "{{count}} tarefas movidas para o fluxo de trabalho",
"movedTasksWorkflowDescription_one": "Mude para o fluxo de trabalho de destino para a ver.",
"movedTasksWorkflowDescription_other": "Mude para o fluxo de trabalho de destino para as ver.",
"sessionActionDelete": "A eliminar sessão",
"sessionActionFailed": "Falha ao {{action}}",
"sessionActionProgress": "A executar: {{action}}...",
"sessionActionResume": "A retomar sessão",
"sessionActionSetPrimary": "Definir como principal",
"sessionActionStop": "A parar sessão",
"sessionActionSuccessful": "{{action}} concluído"
}
+30 -1
View File
@@ -253,5 +253,34 @@
"workItemUnavailable": "工作项不可用。",
"workItemWatches": "工作项监控",
"workItemWatchesDescription": "根据匹配的 Azure DevOps 工作项查询自动创建任务。",
"workItemWatchFieldsRequired": "项目、WIQL、仓库、工作流、步骤、代理和执行器均为必填项。"
"workItemWatchFieldsRequired": "项目、WIQL、仓库、工作流、步骤、代理和执行器均为必填项。",
"prStatusAbandoned": "已放弃",
"prStatusActive": "活动",
"prStatusChangesRequested": "已请求更改",
"prStatusCompleted": "已完成",
"prStatusDraft": "草稿",
"prStatusPolicyFailed": "策略失败",
"prStatusPolicyRunning": "策略运行中",
"prStatusReady": "就绪",
"prStatusWaitingForReview": "等待审查",
"pullRequestStatusAria": "Azure PR {{id}}:{{status}}{{suffix}}",
"defaultActionAddressFeedback": "处理反馈",
"defaultActionAddressFeedbackHint": "应用审查意见",
"defaultActionFixCi": "修复 CI",
"defaultActionFixCiHint": "诊断失败的检查",
"defaultActionImplement": "实施",
"defaultActionImplementHint": "构建并打开 PR",
"defaultActionInvestigate": "调查",
"defaultActionInvestigateHint": "查找根本原因",
"defaultActionReproduce": "复现",
"defaultActionReproduceHint": "记录复现步骤",
"defaultActionReview": "审查",
"defaultActionReviewHint": "阅读差异并标记问题",
"defaultQueryActive": "活动",
"defaultQueryAssignedToMe": "分配给我",
"defaultQueryCompleted": "已完成",
"defaultQueryCreatedByMe": "由我创建",
"defaultQueryOpen": "打开",
"defaultQueryRecentlyUpdated": "最近更新",
"defaultQueryReviewRequested": "已请求审查"
}
+30 -1
View File
@@ -441,5 +441,34 @@
"you": "您",
"youCanUseTheNavigationOr": "你可以使用导航或重新加载以重试。",
"yourAgentImplementsItInThe": "你的代理在 kandev 仓库中实现它,并附带测试",
"youVerifyAndTestTheChange": "你在第二个 kandev 实例中验证并测试该改动"
"youVerifyAndTestTheChange": "你在第二个 kandev 实例中验证并测试该改动",
"failedToRenderDiagram": "图表渲染失败",
"demoAgentMessagesDescription": "目视检查代理消息规范化。每张卡片同时显示渲染结果和原始 JSON。",
"demoAgentMessagesQa": "代理消息质量检查",
"demoErrorLoading": "加载{{mode}}时出错",
"demoEventFile": "事件文件:",
"demoEvents": "事件",
"demoFailedToLoadFiles": "加载文件失败",
"demoFailedToLoadFixtures": "加载测试数据失败",
"demoFailedToLoadMessages": "加载消息失败",
"demoFileOption_one": "{{protocol}} - {{agent}}({{count}} 条消息)",
"demoFileOption_other": "{{protocol}} - {{agent}}({{count}} 条消息)",
"demoFixtureFile": "测试数据文件:",
"demoFixtures": "测试数据",
"demoMessageData": "消息数据",
"demoMessageTypes": "消息类型演示",
"demoMessageTypesDescription": "呈现所有消息类型以供样式设计和测试",
"demoNoFilesFound": "未找到文件",
"demoNoFixtures": "未找到测试数据。请从上方下拉列表中选择文件。",
"demoNoNormalizedEvents": "未找到规范化事件文件。请使用 KANDEV_DEBUG_AGENT_MESSAGES=1 运行代理以生成事件日志。",
"demoNormalizedEvents": "规范化事件",
"demoNormalizedPayload": "规范化内容",
"demoRawInput": "原始输入",
"demoRenderedOutput": "渲染结果:",
"demoShowingFixtures_one": "显示 {{count}} / {{total}} 个测试数据",
"demoShowingFixtures_other": "显示 {{count}} / {{total}} 个测试数据",
"demoShowingMessages_one": "显示 {{count}} / {{total}} 条消息",
"demoShowingMessages_other": "显示 {{count}} / {{total}} 条消息",
"demoTestFixtures": "测试数据",
"demoToolType": "工具类型:"
}
+2 -1
View File
@@ -85,5 +85,6 @@
"switchToUnifiedView": "切换到统一视图",
"toggleCode": "切换代码",
"toggleWordWrap": "切换自动换行",
"underlineCmdU": "下划线(Cmd+U)"
"underlineCmdU": "下划线(Cmd+U)",
"failedToOpenEditor": "打开编辑器失败"
}
+4 -1
View File
@@ -347,5 +347,8 @@
"mrChipAutoMerge": "自动合并",
"mrChipDrawerTitle": "合并请求 !{{mriid}}",
"mrChipDrawerDescription": "流水线状态、审阅状态和合并请求操作。",
"mrChipDrawerClose": "关闭合并请求状态面板"
"mrChipDrawerClose": "关闭合并请求状态面板",
"gitlabRejectedAssociation": "GitLab 拒绝了关联。",
"taskCreatedMrNotLinked": "任务已创建,但未关联合并请求",
"taskCreatedMrNotLinkedDescription": "打开任务并使用“关联 GitLab 合并请求”重试。{{error}}"
}
+2 -1
View File
@@ -91,5 +91,6 @@
"workflow": "工作流",
"workflowStepFallback": "工作流步骤 {{stepId}}",
"columns": "列",
"columnsForWorkflow": "{{name}} 的列"
"columnsForWorkflow": "{{name}} 的列",
"needsReassignment": "需要重新分配"
}
+11 -1
View File
@@ -78,5 +78,15 @@
"walkMeThroughTheseReviewChanges": "带我浏览这些审阅更改",
"walkthrough": "导览",
"walkthroughDiscarded": "导览已丢弃",
"wrapLongLines": "长行自动换行"
"wrapLongLines": "长行自动换行",
"changesDiscarded": "更改已丢弃",
"changesStillLoading": "更改仍在加载中",
"couldNotSendFinding": "无法发送发现项",
"couldNotUpdateFinding": "无法更新发现项",
"discardFailed": "丢弃失败",
"failedToRequestWalkthrough": "请求导览失败",
"findingQueuedForAgent": "发现项已加入代理队列",
"findingSentToAgent": "发现项已发送给代理",
"walkthroughRequestQueued": "导览请求已排队",
"walkthroughRequestSent": "导览请求已发送"
}
+17 -1
View File
@@ -692,5 +692,21 @@
"visualEditorUnavailable": "可视化编辑器不可用",
"vsCodeRemoteSsh": "VS Code Remote SSH",
"whenEnabledInstallYourProjectS": "启用后,会通过仓库设置脚本安装项目依赖(例如运行 <2>{{command}}</2>),以避免缺少类型错误。",
"externalMcpToolListTaskSessions": "按最近启动的顺序列出任务的所有代理会话,以便其他工具通过 ID 定位会话。"
"externalMcpToolListTaskSessions": "按最近启动的顺序列出任务的所有代理会话,以便其他工具通过 ID 定位会话。",
"shortcutBottomTerminal": "切换底部终端",
"shortcutCommandPanel": "命令面板",
"shortcutCommandPanelAlt": "命令面板(备用)",
"shortcutFileSearch": "文件搜索",
"shortcutFocusChatInput": "聚焦聊天输入框",
"shortcutFocusCliChatInput": "聚焦 CLI 聊天输入框",
"shortcutNewTask": "新建任务",
"shortcutOpenTaskPullRequest": "打开任务拉取请求",
"shortcutOpenWorkspacePicker": "打开工作区选择器",
"shortcutQuickChat": "快速聊天",
"shortcutRecentTaskSwitcher": "最近任务切换器",
"shortcutRecentTaskSwitcherBackward": "最近任务切换器(向后)",
"shortcutReverseChatSearch": "反向聊天搜索",
"shortcutTaskContentSearch": "搜索任务内容",
"shortcutTogglePlanMode": "切换计划模式",
"shortcutToggleSidebar": "切换侧边栏"
}
+3 -1
View File
@@ -106,5 +106,7 @@
"viewLabel": "视图:{{name}}",
"whatsNew": "新功能",
"work": "工作",
"workspaceFallback": "工作区"
"workspaceFallback": "工作区",
"taskPreferences": "侧边栏任务偏好设置",
"views": "侧边栏视图"
}
+47 -1
View File
@@ -1516,5 +1516,51 @@
"autopilotDescription": "代理会自主工作,只有在关键决策阻碍进展时才会向父任务询问,然后等待答复再继续。",
"autopilotInfoLabel": "了解自动驾驶模式",
"autopilotSidebarDescription": "自动驾驶任务",
"autopilotChatDescription": "自动驾驶已启用。代理只会向父任务询问关键决策。"
"autopilotChatDescription": "自动驾驶已启用。代理只会向父任务询问关键决策。",
"duplicateRepository": "仓库重复",
"duplicateRepositoryDescription": "{{repository}} 被添加了多次。请删除重复行。",
"repositoryStillBeingVerified": "仍在验证仓库",
"repositoryProviderVerificationFailed": "Kandev 无法验证此提供商 URL。请在仓库行中使用重试。",
"repositoryProviderVerificationPending": "请等待提供商检查完成后再创建任务。",
"failedToCreateFile": "创建文件失败",
"failedToCreateSession": "创建会话失败",
"failedToCreateTask": "创建任务失败",
"failedToDeleteItem": "删除项目失败",
"failedToDeletePlan": "删除计划失败",
"failedToDownloadFile": "下载文件失败",
"failedToFetchPlan": "获取计划失败",
"failedToLoadPlanRevisions": "加载修订版本失败",
"failedToRenameItem": "重命名项目失败",
"failedToRevertPlan": "还原计划失败",
"failedToSavePlan": "保存计划失败",
"failedToStartTaskPlanMode": "以计划模式启动任务失败",
"failedToUpdateTask": "更新任务失败",
"noPullOrMergeRequestLinked": "此任务未关联拉取请求或合并请求",
"sessionEndedCreateNew": "会话已结束。请创建新会话以继续。",
"sessionUnavailableForInput": "会话不可用于输入",
"taskQueued": "任务已排队",
"taskQueuedWipLimit": "工作流步骤已达到 WIP 限制;此任务将在有可用容量时启动。",
"failedToCheckRepositoryStatus": "检查本地仓库状态失败",
"failedToDiscoverRepositories": "发现仓库失败",
"failedToLoadCommitDiff": "加载提交差异失败",
"failedToNestTask": "嵌套任务失败",
"failedToProceedToNextStep": "进入下一步失败",
"failedToStartImplementingPlan": "开始实施计划失败",
"failedToStartImplementationSession": "启动实施会话失败",
"failedToMoveTask": "移动任务失败",
"movedTasksStepDescription_one": "任务现已位于所选步骤中。",
"movedTasksStepDescription_other": "任务现已位于所选步骤中。",
"movedTasksToStep_one": "已将任务移动到步骤",
"movedTasksToStep_other": "已将 {{count}} 个任务移动到步骤",
"movedTasksToWorkflow_one": "已将任务移动到工作流",
"movedTasksToWorkflow_other": "已将 {{count}} 个任务移动到工作流",
"movedTasksWorkflowDescription_one": "切换到目标工作流即可查看。",
"movedTasksWorkflowDescription_other": "切换到目标工作流即可查看。",
"sessionActionDelete": "删除会话中",
"sessionActionFailed": "{{action}}失败",
"sessionActionProgress": "正在{{action}}...",
"sessionActionResume": "恢复会话中",
"sessionActionSetPrimary": "设为主要会话",
"sessionActionStop": "停止会话中",
"sessionActionSuccessful": "{{action}}成功"
}
+115
View File
@@ -0,0 +1,115 @@
---
spec: docs/specs/i18n-second-audit-gaps/spec.md
created: 2026-08-13
status: completed
---
# Implementation Plan: Additional I18n Audit Gaps
## Approach
Externalize the second-audit gaps without translating stable contracts. Plain configuration will
store catalog keys or locale-neutral states; React hooks/components will resolve keys through
`useTranslation`, and operation helpers will receive resolved copy or a call-time translator. Existing
server/domain error messages remain detail text, with localized owned fallbacks used only when no
detail exists. English, pseudo, Portuguese, and Simplified Chinese catalogs must retain key parity.
## Implementation
### Stable-value refactors
- Replace `label` strings in `lib/keyboard/shortcut-overrides.ts` with typed catalog keys. Resolve them
in all Settings consumers without changing shortcut IDs, persistence, or bindings.
- Replace Azure DevOps display strings in `azure-devops-mode-tabs.tsx`, `azure-devops-status.ts`, and
`azure-devops-workspace-defaults.ts` with typed keys/states and update their consumers. Preserve WIQL,
IDs, provider/status values, and prompt templates byte-for-byte.
### Operation and toast copy
- Localize review finding actions, send-to-agent results, review-dialog discard copy, task-plan
fallback errors, walkthrough request states, file-operation fallbacks, task creation/launch results,
and sidebar synchronization titles.
- Complete the source-audit clusters in task/session/file operations, task movement, task creation,
GitHub/GitLab feedback, and related hooks/components. At minimum inspect and address
`use-session-actions.ts`, `use-commit-diff.ts`, `use-task-workflow-move.ts`, `use-implement-fresh.ts`,
`use-plan-actions.ts`, `use-mr-feedback.ts`, `use-task-mr.ts`, `use-file-editors.ts`,
`use-open-session-in-editor.ts`, task-create setup/effects/fresh-branch consent, new-session actions,
file-browser actions, quick task launchers, and task-deleted/removal notifications.
- Use `rg` over toast calls and plain-object `title`, `description`, and `message` fields. Translate only
text proven to reach users; preserve console diagnostics and raw domain/server detail.
### Isolated surfaces and demos
- Localize the task PR shortcut fallback and Mermaid toast title at call/render time.
- Make the orphan swimlane's stable object locale-neutral and inject the localized title into the
rendered step without changing filtering/move behavior.
- Resolve the 21 lint violations in `app/demo/agent-messages/page.tsx` and
`app/demo/messages/page.tsx`; localize page chrome, counts with plural keys, and accessibility copy,
but retain intentional message fixture payloads.
### Guard and catalogs
- Add keys to the narrowest existing namespaces (`review`, `task`, `common`, `settings`, `azuredevops`,
`sidebar`, and others matching surface ownership) across every locale.
- Append every newly migrated path not already covered to `i18nGuardFiles`; never remove entries.
## Mobile Parity
This work changes copy and locale-resolution timing only. Existing desktop/mobile composition,
navigation, overlays, scrolling, and touch targets remain unchanged. The nearest exemplars are the
currently shipped versions of each touched dialog, toast, Settings row, kanban lane, and demo page.
Focused rendered tests at the existing responsive surfaces satisfy mobile parity; no new mobile E2E
is required unless implementation changes structure or interaction.
## TDD And Verification
- Add failing focused assertions before changing voice mapping, Azure/shortcut metadata, orphan-step
derivation, or any helper whose output contract changes.
- Update existing hook/component tests for translated toast payloads and live locale switching where a
component remains mounted across locale changes.
- Run each task's focused Vitest command, then from `apps/web` run:
```bash
pnpm run i18n:check
pnpm run i18n:sweep -- app components hooks lib
pnpm run lint:i18n -- <changed paths>
```
Record every remaining sweep candidate and its intentional untranslated category. Run focused
rendered/browser verification for the touched UI; if unavailable, record the exact blocker.
## Risks
- Translating an error message too early can freeze locale or accidentally alter a stable domain
contract. Keep keys/states separate from presentation and resolve at call/render time.
- Helper exports shared by tests and non-React consumers may require typed translation-key metadata
rather than a hook dependency.
- The broad source audit can include fixture/domain strings that must not be translated; each candidate
needs reachability review rather than mechanical replacement.
## Tasks
- [x] [Task 02: Azure and shortcut metadata](task-02-azure-shortcut-metadata.md)
- [x] [Task 03: Review and plan operations](task-03-review-plan-operations.md)
- [x] [Task 04: Production operation audit](task-04-production-operation-audit.md)
- [x] [Task 05: Isolated UI leaks](task-05-isolated-ui-leaks.md)
- [x] [Task 06: Demo pages and final audit](task-06-demo-pages-final-audit.md)
Task 01 was removed after rebasing onto main because `#2576` removed core Voice Mode in favor of the
Voice plugin. Remaining tasks are sequential because they share locale catalogs and guard
configuration. No implementation subagents are authorized by this plan.
## Verification Results
- Focused Vitest: 48 tests passed across 8 final regression files; earlier expanded runs passed 51
tests across 10 files and 43 tests across 11 files.
- `pnpm run typecheck`: passed.
- Targeted `pnpm run lint:i18n -- <changed paths>`: passed with only specialized-config warnings for
unrelated rule suppressions.
- `pnpm run i18n:check`: passed; 127 pre-existing real-locale advisories and two pre-existing orphan
keys (`task:downloadingPercent`, `task:failedToLoad`) remain.
- `pnpm run i18n:sweep -- app components hooks lib`: completed across 2,445 files. Remaining hits are
intentional agent/demo fixture payloads, WIQL/query syntax, prompt/markdown builders, console
diagnostics, code examples/selectors, provider/domain values, and formatting glyphs. The two plural
findings are known agent-facing CI prompts and must remain English.
- `node scripts/check-guard-allowlist.mjs`: passed with 508 entries and 26 additions.
@@ -0,0 +1,39 @@
---
id: "02-azure-shortcut-metadata"
title: "Localize configuration metadata"
status: done
wave: 2
depends_on: []
plan: "plan.md"
spec: "../../specs/i18n-second-audit-gaps/spec.md"
---
# Task 02: Azure And Shortcut Metadata
## Acceptance
- Azure mode, PR status, default-query, action, and hint labels resolve from keys at render time.
- Shortcut Settings names resolve from keys while IDs, bindings, WIQL, statuses, provider values, and
agent prompt templates remain unchanged.
- Focused tests cover stable metadata and translated consumers.
## Likely Files
`apps/web/components/azure-devops/azure-devops-mode-tabs.tsx`, `azure-devops-status.ts`,
`azure-devops-workspace-defaults.ts`, their consumers/tests, `apps/web/lib/keyboard/shortcut-overrides.ts`,
its tests and Settings consumers, catalogs, guard configuration.
## Verification
```bash
cd apps/web && pnpm test -- --run components/azure-devops/azure-devops-status.test.ts components/azure-devops/azure-devops-presets.test.ts lib/keyboard/shortcut-overrides.test.ts && pnpm run lint:i18n -- components/azure-devops lib/keyboard app/settings
```
## Risks
Configuration objects cross render and persistence boundaries; only display metadata may change shape.
## Results
Azure modes/statuses and unchanged built-in query/action copy resolve from catalog keys; customized
records, IDs, bindings, WIQL, statuses, and prompts remain stable. Focused tests and typecheck passed.
@@ -0,0 +1,38 @@
---
id: "03-review-plan-operations"
title: "Localize review and plan operations"
status: done
wave: 3
depends_on: ["02-azure-shortcut-metadata"]
plan: "plan.md"
spec: "../../specs/i18n-second-audit-gaps/spec.md"
---
# Task 03: Review And Plan Operations
## Acceptance
- Review finding, send-to-agent, review-dialog discard, walkthrough request, and task-plan fallbacks use
active-locale copy at operation time.
- Fetch, save, delete, revision-load, and revert plan fallbacks are all covered.
- Server error detail and agent-facing prompt content remain untranslated.
## Likely Files
The three review files named in the spec, `apps/web/hooks/domains/session/use-task-plan.ts`,
`use-request-changes-walkthrough.ts`, their focused tests, catalogs, and guard configuration.
## Verification
```bash
cd apps/web && pnpm test -- --run hooks/domains/session/use-request-changes-walkthrough.test.ts components/review/review-dialog-pr-state.test.tsx && pnpm run lint:i18n -- hooks/domains/review hooks/domains/session/use-task-plan.ts hooks/domains/session/use-request-changes-walkthrough.ts components/review/review-dialog.tsx
```
## Risks
Callback dependency lists must include translation resolution so live locale changes do not retain stale copy.
## Results
Review, walkthrough, discard, and all five task-plan fallback paths resolve active-locale copy at
operation time while raw server detail and prompt content remain unchanged.
@@ -0,0 +1,40 @@
---
id: "04-production-operation-audit"
title: "Localize production operation copy"
status: done
wave: 4
depends_on: ["03-review-plan-operations"]
plan: "plan.md"
spec: "../../specs/i18n-second-audit-gaps/spec.md"
---
# Task 04: Production Operation Audit
## Acceptance
- File operations, task creation, session/task actions, task movement, GitHub/GitLab feedback, and
sidebar synchronization expose no owned English toast/error copy found by the source audit.
- Unknown owned fallbacks are localized while raw server/domain details and console diagnostics remain unchanged.
- Changed helper logic receives failing tests first and all nearest focused tests pass.
## Likely Files
`apps/web/hooks/use-file-operations.ts`, `use-sidebar-views-sync.ts`, task/session/movement/file hooks,
GitHub/GitLab feedback hooks, `apps/web/components/task-create-dialog-submit.tsx`, task-create helpers,
new-session/file-browser/quick-launch components, tests, catalogs, and guard configuration.
## Verification
```bash
cd apps/web && pnpm test -- --run hooks/use-file-operations.test.ts hooks/use-sidebar-views-sync.test.ts components/task-create-dialog-submit.test.tsx hooks/domains/session/use-session-actions.test.ts hooks/use-task-workflow-move.test.ts && pnpm run lint:i18n -- hooks components/task-create-dialog-submit.tsx components/task-create-dialog-effects.ts components/task-create-dialog-fresh-branch-consent.ts
```
## Risks
The sweep includes tests, fixtures, and domain data; translate only copy with a confirmed user-visible path.
## Results
Localized task/session/file operations, task creation and movement, GitLab association recovery,
repository discovery, editor fallbacks, plan actions, nesting, and sidebar synchronization. Focused
tests, typecheck, source audit, and guard lint passed.
@@ -0,0 +1,37 @@
---
id: "05-isolated-ui-leaks"
title: "Localize isolated UI leaks"
status: done
wave: 5
depends_on: ["04-production-operation-audit"]
plan: "plan.md"
spec: "../../specs/i18n-second-audit-gaps/spec.md"
---
# Task 05: Isolated UI Leaks
## Acceptance
- The task PR shortcut and Mermaid toast use localized owned fallbacks without translating remote details.
- The orphan swimlane title is localized while its sentinel, filtering, and move-target exclusion remain stable.
- Existing focused tests cover toast behavior and orphan-step semantics.
## Likely Files
`apps/web/components/task/task-pr-shortcut.tsx`, `components/shared/mermaid-error-toast.tsx`,
`components/kanban/swimlane-kanban-content.tsx`, their tests, catalogs, and guard configuration.
## Verification
```bash
cd apps/web && pnpm test -- --run components/task/task-pr-shortcut.test.tsx components/shared/mermaid-error-toast.test.tsx components/kanban/orphan-move-target.test.ts && pnpm run lint:i18n -- components/task/task-pr-shortcut.tsx components/shared/mermaid-error-toast.tsx components/kanban/swimlane-kanban-content.tsx
```
## Risks
Do not place translated text into the module-level synthetic workflow-step constant.
## Results
PR shortcut and Mermaid owned fallbacks are localized, and the orphan step remains locale-neutral at
module scope with its title injected during render. Focused tests passed.
@@ -0,0 +1,38 @@
---
id: "06-demo-pages-final-audit"
title: "Localize demos and verify audit"
status: done
wave: 6
depends_on: ["05-isolated-ui-leaks"]
plan: "plan.md"
spec: "../../specs/i18n-second-audit-gaps/spec.md"
---
# Task 06: Demo Pages And Final Audit
## Acceptance
- All 21 demo-page lint violations in page chrome are localized, with count-based plurals and localized accessibility copy.
- Intentional agent-message fixture payloads remain verbatim and every remaining sweep candidate is categorized.
- Catalog parity, module-scope checks, guard lint, and focused rendered verification pass or have an exact recorded blocker.
## Likely Files
`apps/web/app/demo/agent-messages/page.tsx`, `app/demo/messages/page.tsx`, catalogs,
`apps/web/eslint.i18n.options.mjs`, and plan/task result sections.
## Verification
```bash
cd apps/web && pnpm run lint:i18n -- app/demo/agent-messages/page.tsx app/demo/messages/page.tsx && pnpm run i18n:check && pnpm run i18n:sweep -- app components hooks lib
```
## Risks
Fixture content is intentionally visible but not application chrome; document rather than translate those candidates.
## Results
All demo page-chrome violations are externalized with count-based plurals. Fixture payloads remain
verbatim. Required guard lint, catalog checks, sweep, typecheck, and focused tests passed with the
pre-existing advisories documented in the plan.
+56
View File
@@ -0,0 +1,56 @@
---
status: draft
created: 2026-08-13
owner: Kandev
---
# Additional I18n Audit Gaps
## Why
The second localization audit found English display copy that escapes JSX literal linting because it
lives in hooks, plain-object configuration, helper return values, toast payloads, or developer demo
pages. These leaks make locale switching and the pseudo-locale incomplete even though the guarded
JSX migration is complete.
## What
- Review findings, task plans, file operations, task/session actions, task movement, task creation,
sidebar synchronization, walkthrough requests, GitHub/GitLab feedback, and related production
operations resolve user-facing success and failure copy from catalogs when the operation runs.
- Azure DevOps configuration retains WIQL, IDs, provider/status values, and agent prompt templates as
stable data while mode labels, pull-request statuses, default query labels, action labels, and hints
are localized at render time.
- Configurable keyboard shortcuts retain stable IDs and bindings while Settings resolves catalog keys
for display names at render time.
- The review shortcut, Mermaid error toast, orphan swimlane title, and developer demo/QA page chrome
are localized. Agent-message fixture payloads remain intentionally untranslated.
- Newly migrated files are added to `i18nGuardFiles` where they are not already covered. Translation
lookup never occurs at module scope, and plural copy uses `count` with plural catalog entries.
## Scenarios
- **GIVEN** a review, plan, file, task, session, movement, feedback, or synchronization operation
succeeds or fails, **WHEN** the UI reports the result, **THEN** owned fallback copy uses the active
locale while server/domain error details remain unmodified.
- **GIVEN** Azure DevOps or keyboard shortcut configuration is rendered, **WHEN** the locale changes,
**THEN** display labels update without changing stored IDs, values, bindings, WIQL, or prompts.
- **GIVEN** an orphaned workflow task is rendered, **WHEN** the synthetic swimlane appears, **THEN**
its title is localized while `__kandev_orphan__` remains the stable sentinel and is never offered as
a move destination.
- **GIVEN** either developer demo page is rendered, **WHEN** controls, headings, counts, empty/error
states, or accessibility names appear, **THEN** page chrome is localized while intentional agent
fixture content remains verbatim.
- **GIVEN** the affected source trees are swept, **WHEN** remaining candidates are reviewed, **THEN**
every untranslated candidate is documented as domain data, diagnostics, product/code text, or an
agent-facing prompt rather than silently ignored.
## Out Of Scope
- Watcher labels, the unknown-task fallback, and orphan catalog cleanup owned by task
`0e0b8b61-35f9-49fa-9bcd-2cab7484510c`.
- Core Voice Mode copy removed from main by `#2576`; voice input is now plugin-owned.
- Translating console diagnostics, raw server errors, identifiers, product names, code examples,
WIQL, provider/status values, branch/repository data, or agent-facing prompt templates.
- Layout, navigation, touch behavior, viewport branching, or other mobile composition changes.
- Translating fixture payload content whose purpose is to demonstrate agent messages.