From ce82e2a4abbce7e769e2f1c9398024173608b655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Thu, 13 Aug 2026 11:11:46 +0100 Subject: [PATCH] fix(i18n): externalize remaining frontend copy (#2616) Co-authored-by: Kandev Agent --- apps/web/app/demo/agent-messages/page.tsx | 95 +++++++++------ apps/web/app/demo/messages/page.tsx | 8 +- .../azure-devops/azure-devops-mode-tabs.tsx | 16 ++- .../azure-devops/azure-devops-presets.test.ts | 24 ++++ .../azure-devops/azure-devops-presets.ts | 9 +- .../azure-devops/azure-devops-results.tsx | 24 ++-- .../azure-devops/azure-devops-scope-bar.tsx | 15 +-- .../azure-devops/azure-devops-status.test.ts | 20 +-- .../azure-devops/azure-devops-status.ts | 36 ++++-- .../azure-devops-task-pull-request-chip.tsx | 13 +- .../azure-devops-work-item-detail.tsx | 28 +++-- .../azure-devops-workspace-defaults.ts | 67 ++++++++++ .../gitlab/my-gitlab/quick-task-launcher.tsx | 10 +- .../kanban/orphan-move-target.test.ts | 2 +- .../kanban/swimlane-graph2-content.tsx | 7 +- .../swimlane-graph2-orphan-display.test.ts | 2 + .../kanban/swimlane-kanban-content.tsx | 9 +- apps/web/components/review/review-dialog.tsx | 8 +- .../settings/keyboard-shortcuts-card.tsx | 27 ++-- apps/web/components/shared/mermaid-block.tsx | 13 +- .../shared/mermaid-error-toast.test.tsx | 2 +- .../components/shared/mermaid-error-toast.tsx | 9 +- .../components/task-create-dialog-effects.ts | 5 +- ...task-create-dialog-fresh-branch-consent.ts | 5 +- .../components/task-create-dialog-submit.tsx | 52 ++++---- .../task/new-session-form-actions.ts | 5 +- apps/web/components/task/task-pr-shortcut.tsx | 6 +- apps/web/eslint.i18n.options.mjs | 28 +++++ .../domains/kanban/use-implement-fresh.ts | 5 +- .../hooks/domains/kanban/use-plan-actions.ts | 10 +- .../domains/review/use-finding-actions.ts | 8 +- .../review/use-send-finding-to-agent.ts | 12 +- .../hooks/domains/session/use-commit-diff.ts | 8 +- .../use-request-changes-walkthrough.ts | 16 +-- .../domains/session/use-session-actions.ts | 40 ++++-- .../hooks/domains/session/use-task-plan.ts | 27 ++-- apps/web/hooks/use-file-editors.ts | 9 +- apps/web/hooks/use-file-operations.test.ts | 9 +- apps/web/hooks/use-file-operations.ts | 43 ++++--- apps/web/hooks/use-nest-task.ts | 3 +- apps/web/hooks/use-open-session-in-editor.ts | 5 +- apps/web/hooks/use-sidebar-views-sync.ts | 14 ++- apps/web/hooks/use-task-workflow-move.ts | 39 +++--- .../web/lib/keyboard/plugin-shortcuts.test.ts | 3 +- apps/web/lib/keyboard/plugin-shortcuts.ts | 15 ++- .../lib/keyboard/shortcut-overrides.test.ts | 31 +---- apps/web/lib/keyboard/shortcut-overrides.ts | 55 ++++++--- apps/web/src/locales/en/azuredevops.json | 31 ++++- apps/web/src/locales/en/common.json | 31 ++++- apps/web/src/locales/en/editors.json | 3 +- apps/web/src/locales/en/gitlab.json | 5 +- apps/web/src/locales/en/kanban.json | 3 +- apps/web/src/locales/en/review.json | 12 +- apps/web/src/locales/en/settings.json | 18 ++- apps/web/src/locales/en/sidebar.json | 4 +- apps/web/src/locales/en/task.json | 48 +++++++- apps/web/src/locales/pseudo/azuredevops.json | 31 ++++- apps/web/src/locales/pseudo/common.json | 30 ++++- apps/web/src/locales/pseudo/editors.json | 3 +- apps/web/src/locales/pseudo/github.json | 2 +- apps/web/src/locales/pseudo/gitlab.json | 5 +- apps/web/src/locales/pseudo/integrations.json | 10 +- apps/web/src/locales/pseudo/kanban.json | 3 +- apps/web/src/locales/pseudo/review.json | 12 +- apps/web/src/locales/pseudo/settings.json | 18 ++- apps/web/src/locales/pseudo/sidebar.json | 4 +- apps/web/src/locales/pseudo/task.json | 55 ++++++++- apps/web/src/locales/pt-pt/azuredevops.json | 31 ++++- apps/web/src/locales/pt-pt/common.json | 31 ++++- apps/web/src/locales/pt-pt/editors.json | 3 +- apps/web/src/locales/pt-pt/gitlab.json | 5 +- apps/web/src/locales/pt-pt/kanban.json | 3 +- apps/web/src/locales/pt-pt/review.json | 12 +- apps/web/src/locales/pt-pt/settings.json | 18 ++- apps/web/src/locales/pt-pt/sidebar.json | 4 +- apps/web/src/locales/pt-pt/task.json | 48 +++++++- apps/web/src/locales/zh-cn/azuredevops.json | 31 ++++- apps/web/src/locales/zh-cn/common.json | 31 ++++- apps/web/src/locales/zh-cn/editors.json | 3 +- apps/web/src/locales/zh-cn/gitlab.json | 5 +- apps/web/src/locales/zh-cn/kanban.json | 3 +- apps/web/src/locales/zh-cn/review.json | 12 +- apps/web/src/locales/zh-cn/settings.json | 18 ++- apps/web/src/locales/zh-cn/sidebar.json | 4 +- apps/web/src/locales/zh-cn/task.json | 48 +++++++- docs/plans/i18n-second-audit-gaps/plan.md | 115 ++++++++++++++++++ .../task-02-azure-shortcut-metadata.md | 39 ++++++ .../task-03-review-plan-operations.md | 38 ++++++ .../task-04-production-operation-audit.md | 40 ++++++ .../task-05-isolated-ui-leaks.md | 37 ++++++ .../task-06-demo-pages-final-audit.md | 38 ++++++ docs/specs/i18n-second-audit-gaps/spec.md | 56 +++++++++ 92 files changed, 1542 insertions(+), 356 deletions(-) create mode 100644 docs/plans/i18n-second-audit-gaps/plan.md create mode 100644 docs/plans/i18n-second-audit-gaps/task-02-azure-shortcut-metadata.md create mode 100644 docs/plans/i18n-second-audit-gaps/task-03-review-plan-operations.md create mode 100644 docs/plans/i18n-second-audit-gaps/task-04-production-operation-audit.md create mode 100644 docs/plans/i18n-second-audit-gaps/task-05-isolated-ui-leaks.md create mode 100644 docs/plans/i18n-second-audit-gaps/task-06-demo-pages-final-audit.md create mode 100644 docs/specs/i18n-second-audit-gaps/spec.md diff --git a/apps/web/app/demo/agent-messages/page.tsx b/apps/web/app/demo/agent-messages/page.tsx index 10670edb2f..c1254709b3 100644 --- a/apps/web/app/demo/agent-messages/page.tsx +++ b/apps/web/app/demo/agent-messages/page.tsx @@ -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("events"); const [fixtureFiles, setFixtureFiles] = useState([]); const [selectedFixtureFile, setSelectedFixtureFile] = useState(""); @@ -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
-
Rendered Output:
+
+ {t("common:demoRenderedOutput")} +
- - + +
@@ -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 }) {
-
Rendered Output:
+
+ {t("common:demoRenderedOutput")} +
- +
); @@ -262,6 +269,7 @@ function ViewModeTabs({ viewMode: ViewMode; setViewMode: (v: ViewMode) => void; }) { + const { t } = useTranslation("common"); return (
@@ -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")} ))}
@@ -298,28 +306,35 @@ function FiltersBar({ toolFilter: ToolFilter; setToolFilter: (v: ToolFilter) => void; }) { + const { t } = useTranslation("common"); return (
- {viewMode === "events" ? "Event File:" : "Fixture File:"} + {t(viewMode === "events" ? "common:demoEventFile" : "common:demoFixtureFile")}
{viewMode === "fixtures" && (
- Tool Type: + + {t("common:demoToolType")} +
{TOOL_TABS.map((tab) => ( ))}
@@ -356,6 +371,7 @@ function ContentArea({ filteredFixtures: NormalizedFixture[]; messages: Message[]; }) { + const { t } = useTranslation("common"); if (loading) { return (
@@ -367,7 +383,11 @@ function ContentArea({ if (error) { return (
-
Error loading {viewMode}
+
+ {t("common:demoErrorLoading", { + mode: t(viewMode === "events" ? "common:demoEvents" : "common:demoFixtures"), + })} +
{error}
); @@ -376,9 +396,7 @@ function ContentArea({ if (itemCount === 0) { return (
- {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")}
); } @@ -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() {
-

Agent Messages QA

+

{t("common:demoAgentMessagesQa")}

-

- Visual inspection of agent message normalization. Each card shows the rendered output - alongside raw JSON. -

+

{t("common:demoAgentMessagesDescription")}

- 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, + }, + )}
(); const permMsg = dummyMessages.find((m) => m.id === "permission-1"); if (permMsg) { @@ -15,10 +17,8 @@ export default function MessagesDemoPage() {
-

Message Types Demo

-

- All message types rendered for styling and testing -

+

{t("common:demoMessageTypes")}

+

{t("common:demoMessageTypesDescription")}

diff --git a/apps/web/components/azure-devops/azure-devops-mode-tabs.tsx b/apps/web/components/azure-devops/azure-devops-mode-tabs.tsx index 2b9f2697f4..2761dbcf90 100644 --- a/apps/web/components/azure-devops/azure-devops-mode-tabs.tsx +++ b/apps/web/components/azure-devops/azure-devops-mode-tabs.tsx @@ -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 (
{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)} ))}
diff --git a/apps/web/components/azure-devops/azure-devops-presets.test.ts b/apps/web/components/azure-devops/azure-devops-presets.test.ts index ac7fa2501a..f4412b9fd5 100644 --- a/apps/web/components/azure-devops/azure-devops-presets.test.ts +++ b/apps/web/components/azure-devops/azure-devops-presets.test.ts @@ -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"); + }); }); diff --git a/apps/web/components/azure-devops/azure-devops-presets.ts b/apps/web/components/azure-devops/azure-devops-presets.ts index eba0b00a92..1e71cea334 100644 --- a/apps/web/components/azure-devops/azure-devops-presets.ts +++ b/apps/web/components/azure-devops/azure-devops-presets.ts @@ -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"); diff --git a/apps/web/components/azure-devops/azure-devops-results.tsx b/apps/web/components/azure-devops/azure-devops-results.tsx index 5f7c9280f7..486c71be54 100644 --- a/apps/web/components/azure-devops/azure-devops-results.tsx +++ b/apps/web/components/azure-devops/azure-devops-results.tsx @@ -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({ - {actions.map((action) => ( - onSelect(item, action)}> - - {action.label} - {action.hint && {action.hint}} - - - ))} + {actions.map((action) => { + const copy = azureActionDisplayCopy( + itemKind === "workItem" ? "work_item" : "pull_request", + action, + t, + ); + return ( + onSelect(item, action)}> + + {copy.label} + {copy.hint && {copy.hint}} + + + ); + })} ); diff --git a/apps/web/components/azure-devops/azure-devops-scope-bar.tsx b/apps/web/components/azure-devops/azure-devops-scope-bar.tsx index 894ad2034c..835688eeda 100644 --- a/apps/web/components/azure-devops/azure-devops-scope-bar.tsx +++ b/apps/web/components/azure-devops/azure-devops-scope-bar.tsx @@ -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; -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 ( presetsForKind(kind, queryPresets)} + presetsByKind={(kind) => presetsForKind(kind, queryPresets, t)} savedPresets={savedViews} onDeleteSaved={onDeleteSaved} canSaveCurrent={canSaveCurrent} diff --git a/apps/web/components/azure-devops/azure-devops-status.test.ts b/apps/web/components/azure-devops/azure-devops-status.test.ts index 795aafe345..f97c93f0d1 100644 --- a/apps/web/components/azure-devops/azure-devops-status.test.ts +++ b/apps/web/components/azure-devops/azure-devops-status.test.ts @@ -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" }); }); }); diff --git a/apps/web/components/azure-devops/azure-devops-status.ts b/apps/web/components/azure-devops/azure-devops-status.ts index ac1e39eab3..4767e20bc8 100644 --- a/apps/web/components/azure-devops/azure-devops-status.ts +++ b/apps/web/components/azure-devops/azure-devops-status.ts @@ -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" }; } diff --git a/apps/web/components/azure-devops/azure-devops-task-pull-request-chip.tsx b/apps/web/components/azure-devops/azure-devops-task-pull-request-chip.tsx index 5da58bebcc..bd6b4b1227 100644 --- a/apps/web/components/azure-devops/azure-devops-task-pull-request-chip.tsx +++ b/apps/web/components/azure-devops/azure-devops-task-pull-request-chip.tsx @@ -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 ( @@ -36,7 +43,7 @@ export function AzureDevOpsTaskPullRequestChip({ taskId }: { taskId: string | nu PR {first.pullRequestId} - {presentation.label} + {statusLabel} {suffix && {suffix}} @@ -44,7 +51,7 @@ export function AzureDevOpsTaskPullRequestChip({ taskId }: { taskId: string | nu

{first.title}

-

{presentation.label}

+

{statusLabel}

); diff --git a/apps/web/components/azure-devops/azure-devops-work-item-detail.tsx b/apps/web/components/azure-devops/azure-devops-work-item-detail.tsx index ea43dc7112..c41a3a9450 100644 --- a/apps/web/components/azure-devops/azure-devops-work-item-detail.tsx +++ b/apps/web/components/azure-devops/azure-devops-work-item-detail.tsx @@ -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({
- {actions.map((action) => ( - - ))} + {actions.map((action) => { + const copy = azureActionDisplayCopy("work_item", action, t); + return ( + + ); + })}
); diff --git a/apps/web/components/azure-devops/azure-devops-workspace-defaults.ts b/apps/web/components/azure-devops/azure-devops-workspace-defaults.ts index 5c5f927aae..391bd287e8 100644 --- a/apps/web/components/azure-devops/azure-devops-workspace-defaults.ts +++ b/apps/web/components/azure-devops/azure-devops-workspace-defaults.ts @@ -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 = { + "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 = { + "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) }; +} diff --git a/apps/web/components/gitlab/my-gitlab/quick-task-launcher.tsx b/apps/web/components/gitlab/my-gitlab/quick-task-launcher.tsx index 25e58b64c5..3548a74590 100644 --- a/apps/web/components/gitlab/my-gitlab/quick-task-launcher.tsx +++ b/apps/web/components/gitlab/my-gitlab/quick-task-launcher.tsx @@ -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, }); diff --git a/apps/web/components/kanban/orphan-move-target.test.ts b/apps/web/components/kanban/orphan-move-target.test.ts index 4d0234a32b..96d160e71c 100644 --- a/apps/web/components/kanban/orphan-move-target.test.ts +++ b/apps/web/components/kanban/orphan-move-target.test.ts @@ -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", () => { diff --git a/apps/web/components/kanban/swimlane-graph2-content.tsx b/apps/web/components/kanban/swimlane-graph2-content.tsx index 9c2b6ddb7a..e35d8e66d1 100644 --- a/apps/web/components/kanban/swimlane-graph2-content.tsx +++ b/apps/web/components/kanban/swimlane-graph2-content.tsx @@ -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(null); const { displayTasks, displaySteps } = useMemo( - () => getGraph2DisplayState(tasks, steps), - [tasks, steps], + () => getGraph2DisplayState(tasks, steps, t("kanban:needsReassignment")), + [tasks, steps, t], ); const sortedTasks = useMemo( diff --git a/apps/web/components/kanban/swimlane-graph2-orphan-display.test.ts b/apps/web/components/kanban/swimlane-graph2-orphan-display.test.ts index 583deca86a..16ba1add81 100644 --- a/apps/web/components/kanban/swimlane-graph2-orphan-display.test.ts +++ b/apps/web/components/kanban/swimlane-graph2-orphan-display.test.ts @@ -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); diff --git a/apps/web/components/kanban/swimlane-kanban-content.tsx b/apps/web/components/kanban/swimlane-kanban-content.tsx index 3daa930294..2aa7a188d9 100644 --- a/apps/web/components/kanban/swimlane-kanban-content.tsx +++ b/apps/web/components/kanban/swimlane-kanban-content.tsx @@ -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[]) { diff --git a/apps/web/components/review/review-dialog.tsx b/apps/web/components/review/review-dialog.tsx index 54db58aa76..8a489bf0d5 100644 --- a/apps/web/components/review/review-dialog.tsx +++ b/apps/web/components/review/review-dialog.tsx @@ -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", }); } diff --git a/apps/web/components/settings/keyboard-shortcuts-card.tsx b/apps/web/components/settings/keyboard-shortcuts-card.tsx index 89c4b5ff53..0072235aa7 100644 --- a/apps/web/components/settings/keyboard-shortcuts-card.tsx +++ b/apps/web/components/settings/keyboard-shortcuts-card.tsx @@ -231,6 +231,21 @@ function buildConflictLabels(groups: ShortcutConflictGroup[]): Map[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({ (null); const [error, setError] = useState(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, diff --git a/apps/web/components/shared/mermaid-error-toast.test.tsx b/apps/web/components/shared/mermaid-error-toast.test.tsx index c9e819265d..61a9e5b561 100644 --- a/apps/web/components/shared/mermaid-error-toast.test.tsx +++ b/apps/web/components/shared/mermaid-error-toast.test.tsx @@ -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(); diff --git a/apps/web/components/shared/mermaid-error-toast.tsx b/apps/web/components/shared/mermaid-error-toast.tsx index e6989e04ad..f1e3a46809 100644 --- a/apps/web/components/shared/mermaid-error-toast.tsx +++ b/apps/web/components/shared/mermaid-error-toast.tsx @@ -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).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]); } diff --git a/apps/web/components/task-create-dialog-effects.ts b/apps/web/components/task-create-dialog-effects.ts index 55988a35e1..df3deb65c0 100644 --- a/apps/web/components/task-create-dialog-effects.ts +++ b/apps/web/components/task-create-dialog-effects.ts @@ -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([]); diff --git a/apps/web/components/task-create-dialog-fresh-branch-consent.ts b/apps/web/components/task-create-dialog-fresh-branch-consent.ts index 07c6f93ade..a64be2fbc2 100644 --- a/apps/web/components/task-create-dialog-fresh-branch-consent.ts +++ b/apps/web/components/task-create-dialog-fresh-branch-consent.ts @@ -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[0]; type CreateTaskResponse = Awaited>; @@ -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; diff --git a/apps/web/components/task-create-dialog-submit.tsx b/apps/web/components/task-create-dialog-submit.tsx index 5e34c7e318..b9e2f9d35d 100644 --- a/apps/web/components/task-create-dialog-submit.tsx +++ b/apps/web/components/task-create-dialog-submit.tsx @@ -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 { diff --git a/apps/web/components/task/new-session-form-actions.ts b/apps/web/components/task/new-session-form-actions.ts index b971ee76b0..e96e67ee67 100644 --- a/apps/web/components/task/new-session-form-actions.ts +++ b/apps/web/components/task/new-session-form-actions.ts @@ -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; @@ -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 { diff --git a/apps/web/components/task/task-pr-shortcut.tsx b/apps/web/components/task/task-pr-shortcut.tsx index e6184e69a7..be71743049 100644 --- a/apps/web/components/task/task-pr-shortcut.tsx +++ b/apps/web/components/task/task-pr-shortcut.tsx @@ -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); diff --git a/apps/web/eslint.i18n.options.mjs b/apps/web/eslint.i18n.options.mjs index 385c6456d5..869d99d564 100644 --- a/apps/web/eslint.i18n.options.mjs +++ b/apps/web/eslint.i18n.options.mjs @@ -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", ]; diff --git a/apps/web/hooks/domains/kanban/use-implement-fresh.ts b/apps/web/hooks/domains/kanban/use-implement-fresh.ts index 70bd9e5029..54df890a7d 100644 --- a/apps/web/hooks/domains/kanban/use-implement-fresh.ts +++ b/apps/web/hooks/domains/kanban/use-implement-fresh.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, ]); } diff --git a/apps/web/hooks/domains/kanban/use-plan-actions.ts b/apps/web/hooks/domains/kanban/use-plan-actions.ts index 0dcc819671..ff4d9eaef4 100644 --- a/apps/web/hooks/domains/kanban/use-plan-actions.ts +++ b/apps/web/hooks/domains/kanban/use-plan-actions.ts @@ -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 => { 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, ]); } diff --git a/apps/web/hooks/domains/review/use-finding-actions.ts b/apps/web/hooks/domains/review/use-finding-actions.ts index 4125f7e747..f138d6c6dd 100644 --- a/apps/web/hooks/domains/review/use-finding-actions.ts +++ b/apps/web/hooks/domains/review/use-finding-actions.ts @@ -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 { diff --git a/apps/web/hooks/domains/review/use-send-finding-to-agent.ts b/apps/web/hooks/domains/review/use-send-finding-to-agent.ts index 0c88c4b901..064e595cd5 100644 --- a/apps/web/hooks/domains/review/use-send-finding-to-agent.ts +++ b/apps/web/hooks/domains/review/use-send-finding-to-agent.ts @@ -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], ); } diff --git a/apps/web/hooks/domains/session/use-commit-diff.ts b/apps/web/hooks/domains/session/use-commit-diff.ts index ed24450566..dc2bc577ac 100644 --- a/apps/web/hooks/domains/session/use-commit-diff.ts +++ b/apps/web/hooks/domains/session/use-commit-diff.ts @@ -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 | 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(); diff --git a/apps/web/hooks/domains/session/use-request-changes-walkthrough.ts b/apps/web/hooks/domains/session/use-request-changes-walkthrough.ts index d9e9614824..16309b9fa0 100644 --- a/apps/web/hooks/domains/session/use-request-changes-walkthrough.ts +++ b/apps/web/hooks/domains/session/use-request-changes-walkthrough.ts @@ -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]); } diff --git a/apps/web/hooks/domains/session/use-session-actions.ts b/apps/web/hooks/domains/session/use-session-actions.ts index 633a3f35c8..f532980fae 100644 --- a/apps/web/hooks/domains/session/use-session-actions.ts +++ b/apps/web/hooks/domains/session/use-session-actions.ts @@ -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, diff --git a/apps/web/hooks/domains/session/use-task-plan.ts b/apps/web/hooks/domains/session/use-task-plan.ts index ca0f33e78b..ef2f046c15 100644 --- a/apps/web/hooks/domains/session/use-task-plan.ts +++ b/apps/web/hooks/domains/session/use-task-plan.ts @@ -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 => { @@ -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 { diff --git a/apps/web/hooks/use-file-editors.ts b/apps/web/hooks/use-file-editors.ts index 2c84914916..52cf2e4796 100644 --- a/apps/web/hooks/use-file-editors.ts +++ b/apps/web/hooks/use-file-editors.ts @@ -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", }); } diff --git a/apps/web/hooks/use-file-operations.test.ts b/apps/web/hooks/use-file-operations.test.ts index 9bd5ab1c93..9564105480 100644 --- a/apps/web/hooks/use-file-operations.test.ts +++ b/apps/web/hooks/use-file-operations.test.ts @@ -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[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"); diff --git a/apps/web/hooks/use-file-operations.ts b/apps/web/hooks/use-file-operations.ts index fef707e081..f69b9b6f3c 100644 --- a/apps/web/hooks/use-file-operations.ts +++ b/apps/web/hooks/use-file-operations.ts @@ -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["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 { 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( op: () => Promise, title: string, + unknownError: string, toast: ToastFn, ): Promise { 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( 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 => { 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 => { 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 => { 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 { diff --git a/apps/web/hooks/use-nest-task.ts b/apps/web/hooks/use-nest-task.ts index c8dd28c993..4328d91587 100644 --- a/apps/web/hooks/use-nest-task.ts +++ b/apps/web/hooks/use-nest-task.ts @@ -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; 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], diff --git a/apps/web/hooks/use-open-session-in-editor.ts b/apps/web/hooks/use-open-session-in-editor.ts index f63e4efd75..6ac8060bf7 100644 --- a/apps/web/hooks/use-open-session-in-editor.ts +++ b/apps/web/hooks/use-open-session-in-editor.ts @@ -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; diff --git a/apps/web/hooks/use-sidebar-views-sync.ts b/apps/web/hooks/use-sidebar-views-sync.ts index 3269cc0c9e..cfb5e737b8 100644 --- a/apps/web/hooks/use-sidebar-views-sync.ts +++ b/apps/web/hooks/use-sidebar-views-sync.ts @@ -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]); } diff --git a/apps/web/hooks/use-task-workflow-move.ts b/apps/web/hooks/use-task-workflow-move.ts index 3461a0bac7..adcade62bb 100644 --- a/apps/web/hooks/use-task-workflow-move.ts +++ b/apps/web/hooks/use-task-workflow-move.ts @@ -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], ); } diff --git a/apps/web/lib/keyboard/plugin-shortcuts.test.ts b/apps/web/lib/keyboard/plugin-shortcuts.test.ts index 3be222a773..e4a7931cc7 100644 --- a/apps/web/lib/keyboard/plugin-shortcuts.test.ts +++ b/apps/web/lib/keyboard/plugin-shortcuts.test.ts @@ -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, diff --git a/apps/web/lib/keyboard/plugin-shortcuts.ts b/apps/web/lib/keyboard/plugin-shortcuts.ts index 5d26de5ac6..bf6a16b51a 100644 --- a/apps/web/lib/keyboard/plugin-shortcuts.ts +++ b/apps/web/lib/keyboard/plugin-shortcuts.ts @@ -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. */ diff --git a/apps/web/lib/keyboard/shortcut-overrides.test.ts b/apps/web/lib/keyboard/shortcut-overrides.test.ts index bcb349c3e9..b3387a3b5f 100644 --- a/apps/web/lib/keyboard/shortcut-overrides.test.ts +++ b/apps/web/lib/keyboard/shortcut-overrides.test.ts @@ -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); }); }); diff --git a/apps/web/lib/keyboard/shortcut-overrides.ts b/apps/web/lib/keyboard/shortcut-overrides.ts index 77aa5263d1..965b38616d 100644 --- a/apps/web/lib/keyboard/shortcut-overrides.ts +++ b/apps/web/lib/keyboard/shortcut-overrides.ts @@ -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( diff --git a/apps/web/src/locales/en/azuredevops.json b/apps/web/src/locales/en/azuredevops.json index 8fcf011d78..29debf47b3 100644 --- a/apps/web/src/locales/en/azuredevops.json +++ b/apps/web/src/locales/en/azuredevops.json @@ -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" } diff --git a/apps/web/src/locales/en/common.json b/apps/web/src/locales/en/common.json index b8e520bcf0..e39d2c6bf5 100644 --- a/apps/web/src/locales/en/common.json +++ b/apps/web/src/locales/en/common.json @@ -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:" } diff --git a/apps/web/src/locales/en/editors.json b/apps/web/src/locales/en/editors.json index 677fbcb662..d403d75a36 100644 --- a/apps/web/src/locales/en/editors.json +++ b/apps/web/src/locales/en/editors.json @@ -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" } diff --git a/apps/web/src/locales/en/gitlab.json b/apps/web/src/locales/en/gitlab.json index 6f85850007..1363f3fd88 100644 --- a/apps/web/src/locales/en/gitlab.json +++ b/apps/web/src/locales/en/gitlab.json @@ -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}}" } diff --git a/apps/web/src/locales/en/kanban.json b/apps/web/src/locales/en/kanban.json index 467aad6cc5..395b780b02 100644 --- a/apps/web/src/locales/en/kanban.json +++ b/apps/web/src/locales/en/kanban.json @@ -91,5 +91,6 @@ "workflow": "Workflow", "workflowStepFallback": "workflow step {{stepId}}", "columns": "Columns", - "columnsForWorkflow": "Columns for {{name}}" + "columnsForWorkflow": "Columns for {{name}}", + "needsReassignment": "Needs Reassignment" } diff --git a/apps/web/src/locales/en/review.json b/apps/web/src/locales/en/review.json index edcb86b0c4..4974ff48a8 100644 --- a/apps/web/src/locales/en/review.json +++ b/apps/web/src/locales/en/review.json @@ -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" } diff --git a/apps/web/src/locales/en/settings.json b/apps/web/src/locales/en/settings.json index d6c62809cf..baab22d31e 100644 --- a/apps/web/src/locales/en/settings.json +++ b/apps/web/src/locales/en/settings.json @@ -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}} 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" } diff --git a/apps/web/src/locales/en/sidebar.json b/apps/web/src/locales/en/sidebar.json index da83b392f3..098df6a4d4 100644 --- a/apps/web/src/locales/en/sidebar.json +++ b/apps/web/src/locales/en/sidebar.json @@ -106,5 +106,7 @@ "viewLabel": "View: {{name}}", "whatsNew": "What's new", "work": "Work", - "workspaceFallback": "Workspace" + "workspaceFallback": "Workspace", + "taskPreferences": "Sidebar task preferences", + "views": "Sidebar views" } diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json index 7d8bd6571f..fcf9cda835 100644 --- a/apps/web/src/locales/en/task.json +++ b/apps/web/src/locales/en/task.json @@ -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" } diff --git a/apps/web/src/locales/pseudo/azuredevops.json b/apps/web/src/locales/pseudo/azuredevops.json index c577d22fef..ebb0417a28 100644 --- a/apps/web/src/locales/pseudo/azuredevops.json +++ b/apps/web/src/locales/pseudo/azuredevops.json @@ -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ũēśţēď" } diff --git a/apps/web/src/locales/pseudo/common.json b/apps/web/src/locales/pseudo/common.json index 13b71804b9..6e22bfd059 100644 --- a/apps/web/src/locales/pseudo/common.json +++ b/apps/web/src/locales/pseudo/common.json @@ -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": "Ţōōĺ ţŷƥē:" } diff --git a/apps/web/src/locales/pseudo/editors.json b/apps/web/src/locales/pseudo/editors.json index 8c42240321..0b1a6e99c5 100644 --- a/apps/web/src/locales/pseudo/editors.json +++ b/apps/web/src/locales/pseudo/editors.json @@ -85,5 +85,6 @@ "switchToUnifiedView": "Śŵĩţćĥ ţō ũńĩƒĩēď vĩēŵ", "toggleCode": "Ţōĝĝĺē ćōďē", "toggleWordWrap": "Ţōĝĝĺē ŵōŕď ŵŕàƥ", - "underlineCmdU": "Ũńďēŕĺĩńē (Ćḿď+Ũ)" + "underlineCmdU": "Ũńďēŕĺĩńē (Ćḿď+Ũ)", + "failedToOpenEditor": "Ƒàĩĺēď ţō ōƥēń ēďĩţōŕ" } diff --git a/apps/web/src/locales/pseudo/github.json b/apps/web/src/locales/pseudo/github.json index 275751f3f4..bee5f3f6cf 100644 --- a/apps/web/src/locales/pseudo/github.json +++ b/apps/web/src/locales/pseudo/github.json @@ -491,7 +491,7 @@ "selectWorkspace": "Śēĺēćţ ŵōŕķśƥàćē", "selectedRepositories": "Śēĺēćţēď ŕēƥōśĩţōŕĩēś", "selectWorkspaceBeforeRequestingReview": "Śēĺēćţ à ŵōŕķśƥàćē ƀēƒōŕē ŕēqũēśţĩńĝ ŕēvĩēŵ.", - "setTheseExactUrlsCreateAClientSecret": "Śēţ ţĥēśē ēxàćţ ŨŔĹś, ćŕēàţē à ćĺĩēńţ śēćŕēţ àńď ŵēƀĥōōķ śēćŕēţ, àńď ďōŵńĺōàď à ƥŕĩvàţē ķēŷ. Ţĥĩś ōńē-ţĩḿē śēţũƥ ēxƥĩŕēś {{expiresAt}}.", + "setTheseExactUrlsCreateAClientSecret": "Àďď ţĥē ţŵō ũśēŕ àũţĥōŕĩźàţĩōń ćàĺĺƀàćķ ŨŔĹś ĩń ţĥē ōŕďēŕ śĥōŵń, śēţ ţĥē ŕēḿàĩńĩńĝ ēxàćţ vàĺũēś, ćŕēàţē à ćĺĩēńţ śēćŕēţ àńď ŵēƀĥōōķ śēćŕēţ, àńď ďōŵńĺōàď à ƥŕĩvàţē ķēŷ. Ţĥĩś ōńē-ţĩḿē śēţũƥ ēxƥĩŕēś {{expiresAt}}.", "showGithubApiLimits": "Śĥōŵ ĜĩţĤũƀ ÀƤĨ ĺĩḿĩţś", "showLess": "Śĥōŵ ĺēśś", "showMore": "Śĥōŵ ḿōŕē", diff --git a/apps/web/src/locales/pseudo/gitlab.json b/apps/web/src/locales/pseudo/gitlab.json index 00dd1d9bb2..4e8dc78384 100644 --- a/apps/web/src/locales/pseudo/gitlab.json +++ b/apps/web/src/locales/pseudo/gitlab.json @@ -347,5 +347,8 @@ "mrChipAutoMerge": "Àũţō-ḿēŕĝē", "mrChipDrawerTitle": "Ḿēŕĝē ŕēqũēśţ !{{mriid}}", "mrChipDrawerDescription": "Ƥĩƥēĺĩńē śţàţũś, ŕēvĩēŵ śţàţũś, àńď ḿēŕĝē ŕēqũēśţ àćţĩōńś.", - "mrChipDrawerClose": "Ćĺōśē ḿēŕĝē ŕēqũēśţ śţàţũś" + "mrChipDrawerClose": "Ćĺōśē ḿēŕĝē ŕēqũēśţ śţàţũś", + "gitlabRejectedAssociation": "ĜĩţĹàƀ ŕēĵēćţēď ţĥē àśśōćĩàţĩōń.", + "taskCreatedMrNotLinked": "Ţàśķ ćŕēàţēď, ƀũţ ḿēŕĝē ŕēqũēśţ ŵàś ńōţ ĺĩńķēď", + "taskCreatedMrNotLinkedDescription": "Ōƥēń ţĥē ţàśķ àńď ũśē Ĺĩńķ ĜĩţĹàƀ ḿēŕĝē ŕēqũēśţ ţō ŕēţŕŷ. {{error}}" } diff --git a/apps/web/src/locales/pseudo/integrations.json b/apps/web/src/locales/pseudo/integrations.json index 184005c672..cfb1c24274 100644 --- a/apps/web/src/locales/pseudo/integrations.json +++ b/apps/web/src/locales/pseudo/integrations.json @@ -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}} ŕēśũĺţś" } diff --git a/apps/web/src/locales/pseudo/kanban.json b/apps/web/src/locales/pseudo/kanban.json index 566703574f..031a9433cb 100644 --- a/apps/web/src/locales/pseudo/kanban.json +++ b/apps/web/src/locales/pseudo/kanban.json @@ -91,5 +91,6 @@ "workflow": "Ŵōŕķƒĺōŵ", "workflowStepFallback": "ŵōŕķƒĺōŵ śţēƥ {{stepId}}", "columns": "Ćōĺũḿńś", - "columnsForWorkflow": "Ćōĺũḿńś ƒōŕ {{name}}" + "columnsForWorkflow": "Ćōĺũḿńś ƒōŕ {{name}}", + "needsReassignment": "Ńēēďś Ŕēàśśĩĝńḿēńţ" } diff --git a/apps/web/src/locales/pseudo/review.json b/apps/web/src/locales/pseudo/review.json index 578580951c..0b894ed57a 100644 --- a/apps/web/src/locales/pseudo/review.json +++ b/apps/web/src/locales/pseudo/review.json @@ -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ũēśţ śēńţ" } diff --git a/apps/web/src/locales/pseudo/settings.json b/apps/web/src/locales/pseudo/settings.json index 2d22f80114..10c5df94b0 100644 --- a/apps/web/src/locales/pseudo/settings.json +++ b/apps/web/src/locales/pseudo/settings.json @@ -692,5 +692,21 @@ "visualEditorUnavailable": "Vĩśũàĺ ēďĩţōŕ ũńàvàĩĺàƀĺē", "vsCodeRemoteSsh": "VŚ Ćōďē Ŕēḿōţē ŚŚĤ", "whenEnabledInstallYourProjectS": "Ŵĥēń ēńàƀĺēď, ĩńśţàĺĺ ŷōũŕ ƥŕōĵēćţ'ś ďēƥēńďēńćĩēś (ē.ĝ. <2>{{command}} 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": "Ţōĝĝĺē Śĩďēƀàŕ" } diff --git a/apps/web/src/locales/pseudo/sidebar.json b/apps/web/src/locales/pseudo/sidebar.json index 6a92990188..9ba38cded2 100644 --- a/apps/web/src/locales/pseudo/sidebar.json +++ b/apps/web/src/locales/pseudo/sidebar.json @@ -106,5 +106,7 @@ "viewLabel": "Vĩēŵ: {{name}}", "whatsNew": "Ŵĥàţ'ś ńēŵ", "work": "Ŵōŕķ", - "workspaceFallback": "Ŵōŕķśƥàćē" + "workspaceFallback": "Ŵōŕķśƥàćē", + "taskPreferences": "Śĩďēƀàŕ ţàśķ ƥŕēƒēŕēńćēś", + "views": "Śĩďēƀàŕ vĩēŵś" } diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json index c91a3cbd87..9ec6e9a8f4 100644 --- a/apps/web/src/locales/pseudo/task.json +++ b/apps/web/src/locales/pseudo/task.json @@ -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}} śũććēśśƒũĺ" } diff --git a/apps/web/src/locales/pt-pt/azuredevops.json b/apps/web/src/locales/pt-pt/azuredevops.json index 5c4f65f8d9..3dddb55aad 100644 --- a/apps/web/src/locales/pt-pt/azuredevops.json +++ b/apps/web/src/locales/pt-pt/azuredevops.json @@ -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" } diff --git a/apps/web/src/locales/pt-pt/common.json b/apps/web/src/locales/pt-pt/common.json index b2c69907fe..a5f169e58c 100644 --- a/apps/web/src/locales/pt-pt/common.json +++ b/apps/web/src/locales/pt-pt/common.json @@ -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:" } diff --git a/apps/web/src/locales/pt-pt/editors.json b/apps/web/src/locales/pt-pt/editors.json index 41f1cee91e..132150956b 100644 --- a/apps/web/src/locales/pt-pt/editors.json +++ b/apps/web/src/locales/pt-pt/editors.json @@ -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" } diff --git a/apps/web/src/locales/pt-pt/gitlab.json b/apps/web/src/locales/pt-pt/gitlab.json index c343072756..0bd53740e3 100644 --- a/apps/web/src/locales/pt-pt/gitlab.json +++ b/apps/web/src/locales/pt-pt/gitlab.json @@ -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}}" } diff --git a/apps/web/src/locales/pt-pt/kanban.json b/apps/web/src/locales/pt-pt/kanban.json index 94efbf2ec6..0d429b4447 100644 --- a/apps/web/src/locales/pt-pt/kanban.json +++ b/apps/web/src/locales/pt-pt/kanban.json @@ -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" } diff --git a/apps/web/src/locales/pt-pt/review.json b/apps/web/src/locales/pt-pt/review.json index f25c8780f1..5184401829 100644 --- a/apps/web/src/locales/pt-pt/review.json +++ b/apps/web/src/locales/pt-pt/review.json @@ -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" } diff --git a/apps/web/src/locales/pt-pt/settings.json b/apps/web/src/locales/pt-pt/settings.json index 7b26a5ae49..aae47eafea 100644 --- a/apps/web/src/locales/pt-pt/settings.json +++ b/apps/web/src/locales/pt-pt/settings.json @@ -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}} 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}} 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" } diff --git a/apps/web/src/locales/pt-pt/sidebar.json b/apps/web/src/locales/pt-pt/sidebar.json index dc407021ca..f5720c0e92 100644 --- a/apps/web/src/locales/pt-pt/sidebar.json +++ b/apps/web/src/locales/pt-pt/sidebar.json @@ -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" } diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json index d0212631d5..c64f078e80 100644 --- a/apps/web/src/locales/pt-pt/task.json +++ b/apps/web/src/locales/pt-pt/task.json @@ -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" } diff --git a/apps/web/src/locales/zh-cn/azuredevops.json b/apps/web/src/locales/zh-cn/azuredevops.json index ea4ba575f5..4f09d3125b 100644 --- a/apps/web/src/locales/zh-cn/azuredevops.json +++ b/apps/web/src/locales/zh-cn/azuredevops.json @@ -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": "已请求审查" } diff --git a/apps/web/src/locales/zh-cn/common.json b/apps/web/src/locales/zh-cn/common.json index eb96399834..21d7f38dd7 100644 --- a/apps/web/src/locales/zh-cn/common.json +++ b/apps/web/src/locales/zh-cn/common.json @@ -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": "工具类型:" } diff --git a/apps/web/src/locales/zh-cn/editors.json b/apps/web/src/locales/zh-cn/editors.json index e97ae5426d..7c68843319 100644 --- a/apps/web/src/locales/zh-cn/editors.json +++ b/apps/web/src/locales/zh-cn/editors.json @@ -85,5 +85,6 @@ "switchToUnifiedView": "切换到统一视图", "toggleCode": "切换代码", "toggleWordWrap": "切换自动换行", - "underlineCmdU": "下划线(Cmd+U)" + "underlineCmdU": "下划线(Cmd+U)", + "failedToOpenEditor": "打开编辑器失败" } diff --git a/apps/web/src/locales/zh-cn/gitlab.json b/apps/web/src/locales/zh-cn/gitlab.json index 2f02ba9281..a077c4159b 100644 --- a/apps/web/src/locales/zh-cn/gitlab.json +++ b/apps/web/src/locales/zh-cn/gitlab.json @@ -347,5 +347,8 @@ "mrChipAutoMerge": "自动合并", "mrChipDrawerTitle": "合并请求 !{{mriid}}", "mrChipDrawerDescription": "流水线状态、审阅状态和合并请求操作。", - "mrChipDrawerClose": "关闭合并请求状态面板" + "mrChipDrawerClose": "关闭合并请求状态面板", + "gitlabRejectedAssociation": "GitLab 拒绝了关联。", + "taskCreatedMrNotLinked": "任务已创建,但未关联合并请求", + "taskCreatedMrNotLinkedDescription": "打开任务并使用“关联 GitLab 合并请求”重试。{{error}}" } diff --git a/apps/web/src/locales/zh-cn/kanban.json b/apps/web/src/locales/zh-cn/kanban.json index 2a3178cd37..7b65a85ef6 100644 --- a/apps/web/src/locales/zh-cn/kanban.json +++ b/apps/web/src/locales/zh-cn/kanban.json @@ -91,5 +91,6 @@ "workflow": "工作流", "workflowStepFallback": "工作流步骤 {{stepId}}", "columns": "列", - "columnsForWorkflow": "{{name}} 的列" + "columnsForWorkflow": "{{name}} 的列", + "needsReassignment": "需要重新分配" } diff --git a/apps/web/src/locales/zh-cn/review.json b/apps/web/src/locales/zh-cn/review.json index b306d8c397..51cda17484 100644 --- a/apps/web/src/locales/zh-cn/review.json +++ b/apps/web/src/locales/zh-cn/review.json @@ -78,5 +78,15 @@ "walkMeThroughTheseReviewChanges": "带我浏览这些审阅更改", "walkthrough": "导览", "walkthroughDiscarded": "导览已丢弃", - "wrapLongLines": "长行自动换行" + "wrapLongLines": "长行自动换行", + "changesDiscarded": "更改已丢弃", + "changesStillLoading": "更改仍在加载中", + "couldNotSendFinding": "无法发送发现项", + "couldNotUpdateFinding": "无法更新发现项", + "discardFailed": "丢弃失败", + "failedToRequestWalkthrough": "请求导览失败", + "findingQueuedForAgent": "发现项已加入代理队列", + "findingSentToAgent": "发现项已发送给代理", + "walkthroughRequestQueued": "导览请求已排队", + "walkthroughRequestSent": "导览请求已发送" } diff --git a/apps/web/src/locales/zh-cn/settings.json b/apps/web/src/locales/zh-cn/settings.json index b02d1509d5..9e34d0c97b 100644 --- a/apps/web/src/locales/zh-cn/settings.json +++ b/apps/web/src/locales/zh-cn/settings.json @@ -692,5 +692,21 @@ "visualEditorUnavailable": "可视化编辑器不可用", "vsCodeRemoteSsh": "VS Code Remote SSH", "whenEnabledInstallYourProjectS": "启用后,会通过仓库设置脚本安装项目依赖(例如运行 <2>{{command}}),以避免缺少类型错误。", - "externalMcpToolListTaskSessions": "按最近启动的顺序列出任务的所有代理会话,以便其他工具通过 ID 定位会话。" + "externalMcpToolListTaskSessions": "按最近启动的顺序列出任务的所有代理会话,以便其他工具通过 ID 定位会话。", + "shortcutBottomTerminal": "切换底部终端", + "shortcutCommandPanel": "命令面板", + "shortcutCommandPanelAlt": "命令面板(备用)", + "shortcutFileSearch": "文件搜索", + "shortcutFocusChatInput": "聚焦聊天输入框", + "shortcutFocusCliChatInput": "聚焦 CLI 聊天输入框", + "shortcutNewTask": "新建任务", + "shortcutOpenTaskPullRequest": "打开任务拉取请求", + "shortcutOpenWorkspacePicker": "打开工作区选择器", + "shortcutQuickChat": "快速聊天", + "shortcutRecentTaskSwitcher": "最近任务切换器", + "shortcutRecentTaskSwitcherBackward": "最近任务切换器(向后)", + "shortcutReverseChatSearch": "反向聊天搜索", + "shortcutTaskContentSearch": "搜索任务内容", + "shortcutTogglePlanMode": "切换计划模式", + "shortcutToggleSidebar": "切换侧边栏" } diff --git a/apps/web/src/locales/zh-cn/sidebar.json b/apps/web/src/locales/zh-cn/sidebar.json index b8e255a13e..d54802c3a0 100644 --- a/apps/web/src/locales/zh-cn/sidebar.json +++ b/apps/web/src/locales/zh-cn/sidebar.json @@ -106,5 +106,7 @@ "viewLabel": "视图:{{name}}", "whatsNew": "新功能", "work": "工作", - "workspaceFallback": "工作区" + "workspaceFallback": "工作区", + "taskPreferences": "侧边栏任务偏好设置", + "views": "侧边栏视图" } diff --git a/apps/web/src/locales/zh-cn/task.json b/apps/web/src/locales/zh-cn/task.json index c63799886e..b9f9e55f11 100644 --- a/apps/web/src/locales/zh-cn/task.json +++ b/apps/web/src/locales/zh-cn/task.json @@ -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}}成功" } diff --git a/docs/plans/i18n-second-audit-gaps/plan.md b/docs/plans/i18n-second-audit-gaps/plan.md new file mode 100644 index 0000000000..6e11341ad6 --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/plan.md @@ -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 -- +``` + +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 -- `: 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. diff --git a/docs/plans/i18n-second-audit-gaps/task-02-azure-shortcut-metadata.md b/docs/plans/i18n-second-audit-gaps/task-02-azure-shortcut-metadata.md new file mode 100644 index 0000000000..baf64810fe --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/task-02-azure-shortcut-metadata.md @@ -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. diff --git a/docs/plans/i18n-second-audit-gaps/task-03-review-plan-operations.md b/docs/plans/i18n-second-audit-gaps/task-03-review-plan-operations.md new file mode 100644 index 0000000000..b1219d51db --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/task-03-review-plan-operations.md @@ -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. diff --git a/docs/plans/i18n-second-audit-gaps/task-04-production-operation-audit.md b/docs/plans/i18n-second-audit-gaps/task-04-production-operation-audit.md new file mode 100644 index 0000000000..a4210cdbf2 --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/task-04-production-operation-audit.md @@ -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. diff --git a/docs/plans/i18n-second-audit-gaps/task-05-isolated-ui-leaks.md b/docs/plans/i18n-second-audit-gaps/task-05-isolated-ui-leaks.md new file mode 100644 index 0000000000..030b593cb6 --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/task-05-isolated-ui-leaks.md @@ -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. diff --git a/docs/plans/i18n-second-audit-gaps/task-06-demo-pages-final-audit.md b/docs/plans/i18n-second-audit-gaps/task-06-demo-pages-final-audit.md new file mode 100644 index 0000000000..a6893c2865 --- /dev/null +++ b/docs/plans/i18n-second-audit-gaps/task-06-demo-pages-final-audit.md @@ -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. diff --git a/docs/specs/i18n-second-audit-gaps/spec.md b/docs/specs/i18n-second-audit-gaps/spec.md new file mode 100644 index 0000000000..9e1b8ac75e --- /dev/null +++ b/docs/specs/i18n-second-audit-gaps/spec.md @@ -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.