feat(i18n): localize editors and quick-chat (#2300)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-06 01:41:04 +02:00
committed by GitHub
parent d57ddfb9cb
commit ed912b2683
32 changed files with 767 additions and 258 deletions
@@ -8,6 +8,7 @@ import { IconCheck, IconCopy } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { getCodeMirrorExtension } from "@/lib/languages";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { useTranslation } from "react-i18next";
type CodeBlockProps = {
children: React.ReactNode;
@@ -21,6 +22,7 @@ const getLanguageExtension = (language?: string) => {
};
export function CodeMirrorCodeBlock({ children, className }: CodeBlockProps) {
const { t } = useTranslation();
const { copied, copy } = useCopyToClipboard();
const { theme, systemTheme } = useTheme();
const effectiveTheme = theme === "system" ? systemTheme : theme;
@@ -52,7 +54,7 @@ export function CodeMirrorCodeBlock({ children, className }: CodeBlockProps) {
"opacity-0 group-hover/code-block:opacity-100",
"cursor-pointer",
)}
title="Copy code"
title={t("editors:copyCode")}
>
{copied ? (
<IconCheck className="h-3 w-3 text-green-400" />
@@ -33,6 +33,7 @@ import {
revealCodeMirrorCursor,
revealPendingCodeMirrorCursor,
} from "./codemirror-cursor-navigation";
import { useTranslation } from "react-i18next";
const SAVE_SHORTCUT =
typeof navigator !== "undefined" && navigator.platform.includes("Mac") ? "\u2318" : "Ctrl";
@@ -67,14 +68,13 @@ function CodeMirrorCommentBadge({
sessionId?: string;
commentCount: number;
}) {
const { t } = useTranslation();
if (!enableComments || !sessionId || commentCount <= 0) return null;
return (
<div className="flex items-center gap-1 px-2 py-1 text-xs text-primary">
<IconMessagePlus className="h-3.5 w-3.5" />
<span>
{commentCount} comment{commentCount > 1 ? "s" : ""}
</span>
<span>{t("editors:commentCount", { count: commentCount })}</span>
</div>
);
}
@@ -86,6 +86,7 @@ function CodeMirrorWrapButton({
wrapEnabled: boolean;
onToggleWrap: () => void;
}) {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -102,7 +103,9 @@ function CodeMirrorWrapButton({
)}
</Button>
</TooltipTrigger>
<TooltipContent>{wrapEnabled ? "Disable word wrap" : "Enable word wrap"}</TooltipContent>
<TooltipContent>
{wrapEnabled ? t("editors:disableWordWrap") : t("editors:enableWordWrap")}
</TooltipContent>
</Tooltip>
);
}
@@ -114,6 +117,7 @@ function CodeMirrorReloadButton({
hasRemoteUpdate?: boolean;
onReloadFromAgent?: () => void;
}) {
const { t } = useTranslation();
if (!hasRemoteUpdate || !onReloadFromAgent) return null;
return (
@@ -126,15 +130,16 @@ function CodeMirrorReloadButton({
onClick={onReloadFromAgent}
>
<IconRefresh className="h-3.5 w-3.5" />
Reload
{t("editors:reload")}
</Button>
</TooltipTrigger>
<TooltipContent>Apply latest agent changes to this file</TooltipContent>
<TooltipContent>{t("editors:applyLatestAgentChangesToFile")}</TooltipContent>
</Tooltip>
);
}
function CodeMirrorDeleteButton({ onDelete }: { onDelete?: () => void }) {
const { t } = useTranslation();
if (!onDelete) return null;
return (
@@ -149,7 +154,7 @@ function CodeMirrorDeleteButton({ onDelete }: { onDelete?: () => void }) {
<IconTrash className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete file</TooltipContent>
<TooltipContent>{t("editors:deleteFile")}</TooltipContent>
</Tooltip>
);
}
@@ -163,6 +168,7 @@ function CodeMirrorSaveButton({
isSaving: boolean;
onSave: () => void;
}) {
const { t } = useTranslation();
return (
<Button
size="sm"
@@ -174,12 +180,12 @@ function CodeMirrorSaveButton({
{isSaving ? (
<>
<IconLoader2 className="h-4 w-4 animate-spin" />
Saving...
{t("editors:saving")}
</>
) : (
<>
<IconDeviceFloppy className="h-4 w-4" />
Save
{t("common:save")}
<span className="text-xs text-muted-foreground">({SAVE_SHORTCUT}+S)</span>
</>
)}
@@ -188,6 +194,7 @@ function CodeMirrorSaveButton({
}
function CodeMirrorMarkdownPreviewButton({ onToggle }: { onToggle: () => void }) {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -201,7 +208,7 @@ function CodeMirrorMarkdownPreviewButton({ onToggle }: { onToggle: () => void })
<IconEye className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Preview markdown</TooltipContent>
<TooltipContent>{t("editors:previewMarkdown")}</TooltipContent>
</Tooltip>
);
}
@@ -297,6 +304,7 @@ function CodeMirrorToolbar({
type CodeMirrorEditorState = ReturnType<typeof useCodeMirrorEditorState>;
function CodeMirrorOverlays({ state }: { state: CodeMirrorEditorState }) {
const { t } = useTranslation();
return (
<>
{state.floatingButtonPos && !state.textSelection && (
@@ -309,7 +317,7 @@ function CodeMirrorOverlays({ state }: { state: CodeMirrorEditorState }) {
onClick={state.handleFloatingButtonClick}
>
<IconMessagePlus className="h-3.5 w-3.5" />
Comment
{t("editors:comment")}
</Button>
)}
{state.textSelection && (
@@ -12,6 +12,9 @@ import type { DiffComment } from "@/lib/diff/types";
import { computeLineDiffStats } from "@/lib/diff";
import { useToast } from "@/components/toast-provider";
import { useCommandPanelOpen } from "@/lib/commands/command-registry";
import { useTranslation } from "react-i18next";
// Resolved inside `toDOM()`, which is a CodeMirror gutter callback with no hook scope.
import { t as moduleT } from "@/lib/i18n";
// ---------------------------------------------------------------------------
// Types
@@ -70,7 +73,7 @@ class CommentGutterMarker extends GutterMarker {
toDOM() {
const marker = document.createElement("div");
marker.className = "cm-comment-gutter-marker";
marker.title = `${this.lineComments.length} comment${this.lineComments.length > 1 ? "s" : ""} - click to view`;
marker.title = moduleT("editors:commentMarkerTitle", { count: this.lineComments.length });
if (this.isFirstLine) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "12");
@@ -198,6 +201,7 @@ type UseCodeMirrorEditorStateOpts = {
// eslint-disable-next-line max-lines-per-function
export function useCodeMirrorEditorState(opts: UseCodeMirrorEditorStateOpts) {
const { t } = useTranslation();
const {
path,
originalContent,
@@ -469,8 +473,8 @@ export function useCodeMirrorEditorState(opts: UseCodeMirrorEditorStateOpts) {
const comment = createCommentFromSelection(annotation);
if (comment) {
toast({
title: "Comment added",
description: "Your comment will be sent with your next message.",
title: t("editors:commentAdded"),
description: t("editors:commentWillBeSentWithNextMessage"),
});
}
},
@@ -484,13 +488,13 @@ export function useCodeMirrorEditorState(opts: UseCodeMirrorEditorStateOpts) {
try {
const { queued } = await runComment(comment);
toast({
title: "Comment sent",
description: queued ? "Queued for the agent." : "Sent to the agent.",
title: t("editors:commentSent"),
description: queued ? t("editors:queuedForTheAgent") : t("editors:sentToTheAgent"),
});
} catch {
toast({
title: "Failed to send comment",
description: "Please try again.",
title: t("editors:failedToSendComment"),
description: t("editors:pleaseTryAgain"),
variant: "error",
});
}
@@ -512,7 +516,7 @@ export function useCodeMirrorEditorState(opts: UseCodeMirrorEditorStateOpts) {
const nextComments = view.comments.filter((comment) => comment.id !== commentId);
return nextComments.length > 0 ? { ...view, comments: nextComments } : null;
});
toast({ title: "Comment deleted" });
toast({ title: t("editors:commentDeleted") });
},
[sessionId, removeComment, toast],
);
@@ -529,7 +533,7 @@ export function useCodeMirrorEditorState(opts: UseCodeMirrorEditorStateOpts) {
),
};
});
toast({ title: "Comment updated" });
toast({ title: t("editors:commentUpdated") });
},
[toast, updateComment],
);
@@ -17,6 +17,7 @@ import { useOpenSessionFolder } from "@/hooks/use-open-session-folder";
import { useAppStore } from "@/components/state-provider";
import type { EditorOption } from "@/lib/types/http";
import { copyToClipboard } from "@/lib/utils/copy-to-clipboard";
import { useTranslation } from "react-i18next";
type FileActionsDropdownProps = {
/** File path to open / copy */
@@ -104,6 +105,7 @@ function useFileActions({
}
export function FileActionsMenuItems(props: FileActionsMenuItemsProps) {
const { t } = useTranslation();
const { defaultEditorId, enabledEditors, handleCopyPath, handleOpenFolder, handleOpenInEditor } =
useFileActions(props);
@@ -118,23 +120,25 @@ export function FileActionsMenuItems(props: FileActionsMenuItemsProps) {
<IconExternalLink className="h-3.5 w-3.5" />
{editor.name}
{editor.id === defaultEditorId && (
<span className="ml-auto text-[10px] text-muted-foreground">default</span>
<span className="ml-auto text-[10px] text-muted-foreground">
{t("editors:default")}
</span>
)}
</DropdownMenuItem>
))}
{enabledEditors.length === 0 && (
<DropdownMenuItem disabled className="text-xs">
No editors configured
{t("editors:noEditorsConfigured")}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer text-xs" onSelect={handleCopyPath}>
<IconCopy className="h-3.5 w-3.5" />
Copy path
{t("editors:copyPath")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer text-xs" onSelect={handleOpenFolder}>
<IconFolderShare className="h-3.5 w-3.5" />
Open folder
{t("editors:openFolder")}
</DropdownMenuItem>
</>
);
@@ -146,6 +150,7 @@ export function FileActionsDropdown({
size = "xs",
onCopied,
}: FileActionsDropdownProps) {
const { t } = useTranslation();
const btnClass = fileActionsButtonClass(size);
const iconClass = size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4";
@@ -159,7 +164,7 @@ export function FileActionsDropdown({
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Open with...</TooltipContent>
<TooltipContent>{t("editors:openWith")}</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-44">
<FileActionsMenuItems filePath={filePath} sessionId={sessionId} onCopied={onCopied} />
@@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { copyToClipboard } from "@/lib/utils/copy-to-clipboard";
import { useTranslation } from "react-i18next";
export type ContextMenuState = {
x: number;
@@ -34,6 +35,7 @@ export function DiffViewerContextMenu({
onRevert,
filePath,
}: DiffViewerContextMenuProps) {
const { t } = useTranslation();
return (
<div
role="menu"
@@ -42,7 +44,7 @@ export function DiffViewerContextMenu({
onMouseDown={(e) => e.stopPropagation()}
>
<button role="menuitem" className={itemCls} onClick={onCopyAllChanged}>
Copy all changed lines
{t("editors:copyAllChangedLines")}
</button>
{contextMenu.isChangedLine && (
<button
@@ -53,7 +55,7 @@ export function DiffViewerContextMenu({
onClose();
}}
>
Copy line {contextMenu.lineNumber}
{t("editors:copyLine", { lineNumber: contextMenu.lineNumber })}
</button>
)}
{onRevert && (
@@ -70,7 +72,7 @@ export function DiffViewerContextMenu({
onClose();
}}
>
Revert all changes
{t("editors:revertAllChanges")}
</button>
</>
)}
@@ -15,6 +15,7 @@ import type { FileDiffData } from "@/lib/diff/types";
import type { ViewMode } from "@/hooks/use-global-view-mode";
import { ExternalVcsFileLink } from "@/components/editors/external-vcs-file-link";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { useTranslation } from "react-i18next";
interface DiffViewerToolbarProps {
data: FileDiffData;
@@ -57,6 +58,7 @@ function DiffViewerToggleButtons({
| "globalViewMode"
| "setGlobalViewMode"
>) {
const { t } = useTranslation();
return (
<>
<Tooltip>
@@ -74,7 +76,9 @@ function DiffViewerToggleButtons({
)}
</Button>
</TooltipTrigger>
<TooltipContent>{foldUnchanged ? "Show all lines" : "Fold unchanged lines"}</TooltipContent>
<TooltipContent>
{foldUnchanged ? t("editors:showAllLines") : t("editors:foldUnchangedLines")}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -87,7 +91,7 @@ function DiffViewerToggleButtons({
<IconTextWrap className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Toggle word wrap</TooltipContent>
<TooltipContent>{t("editors:toggleWordWrap")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -105,7 +109,9 @@ function DiffViewerToggleButtons({
</Button>
</TooltipTrigger>
<TooltipContent>
{globalViewMode === "split" ? "Switch to unified view" : "Switch to split view"}
{globalViewMode === "split"
? t("editors:switchToUnifiedView")
: t("editors:switchToSplitView")}
</TooltipContent>
</Tooltip>
</>
@@ -132,6 +138,7 @@ function DiffViewerActions({
publishedBranch,
baseBranch,
}: DiffViewerActionsProps) {
const { t } = useTranslation();
const { isMobile } = useResponsiveBreakpoint();
return (
<div className="flex items-center gap-1">
@@ -153,7 +160,7 @@ function DiffViewerActions({
<IconCopy className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Copy diff</TooltipContent>
<TooltipContent>{t("editors:copyDiff")}</TooltipContent>
</Tooltip>
<DiffViewerToggleButtons
foldUnchanged={foldUnchanged}
@@ -175,7 +182,7 @@ function DiffViewerActions({
<IconArrowBackUp className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Revert changes</TooltipContent>
<TooltipContent>{t("editors:revertChanges")}</TooltipContent>
</Tooltip>
)}
{onOpenFile && (
@@ -190,7 +197,7 @@ function DiffViewerActions({
<IconPencil className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
<TooltipContent>{t("common:edit")}</TooltipContent>
</Tooltip>
)}
</div>
@@ -8,6 +8,7 @@ import { getMonacoLanguageFromName } from "@/lib/editor/language-map";
import { EDITOR_FONT_FAMILY } from "@/lib/theme/editor-theme";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { initMonacoThemes } from "./monaco-init";
import { useTranslation } from "react-i18next";
initMonacoThemes();
@@ -17,6 +18,7 @@ type MonacoCodeBlockProps = {
};
export function MonacoCodeBlock({ children, className }: MonacoCodeBlockProps) {
const { t } = useTranslation();
const { copied, copy } = useCopyToClipboard();
const { resolvedTheme } = useTheme();
@@ -39,7 +41,7 @@ export function MonacoCodeBlock({ children, className }: MonacoCodeBlockProps) {
"opacity-0 group-hover/code-block:opacity-100",
"cursor-pointer",
)}
title="Copy code"
title={t("editors:copyCode")}
>
{copied ? (
<IconCheck className="h-3 w-3 text-green-400" />
@@ -16,6 +16,7 @@ import { useMonacoEditorComments } from "./use-monaco-editor-state";
import { useMonacoEditorLsp, useMonacoDiffDecorations } from "./use-monaco-editor-lsp";
import { useMonacoWalkthroughRange } from "./use-monaco-walkthrough-range";
import { initMonacoThemes } from "./monaco-init";
import { useTranslation } from "react-i18next";
initMonacoThemes();
@@ -172,7 +173,17 @@ function useMonacoCodeEditorSetup(props: MonacoCodeEditorProps) {
return { contentRef, wrapperRef, language, state, lsp, diffStats, options };
}
function EditorLoading() {
const { t } = useTranslation();
return (
<div className="flex h-full items-center justify-center text-muted-foreground text-sm">
{t("editors:loadingEditor")}
</div>
);
}
export function MonacoCodeEditor(props: MonacoCodeEditorProps) {
const { t } = useTranslation();
const {
path,
content,
@@ -235,11 +246,7 @@ export function MonacoCodeEditor(props: MonacoCodeEditorProps) {
onMount={state.handleEditorDidMount}
keepCurrentModel
options={options}
loading={
<div className="flex h-full items-center justify-center text-muted-foreground text-sm">
Loading editor...
</div>
}
loading={<EditorLoading />}
/>
{walkthroughRange ? (
<div
@@ -266,7 +273,7 @@ export function MonacoCodeEditor(props: MonacoCodeEditorProps) {
onClick={state.handleFloatingButtonClick}
>
<IconMessagePlus className="h-3.5 w-3.5" />
Comment
{t("editors:comment")}
</Button>
)}
</div>
@@ -17,6 +17,7 @@ import { useDiffViewerComments } from "./use-diff-viewer-comments";
import { useGlobalFolding } from "./use-global-folding";
import { resolveDiffContent, buildDiffEditorOptions } from "./diff-viewer-helpers";
import { initMonacoThemes } from "./monaco-init";
import { useTranslation } from "react-i18next";
initMonacoThemes();
@@ -145,6 +146,7 @@ function useMonacoDiffViewerState(props: MonacoDiffViewerProps) {
}
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
const { t } = useTranslation();
const { className, compact = false, hideHeader = false, onOpenFile, onRevert } = props;
const state = useMonacoDiffViewerState(props);
const { wrapperRef, hasDiff, contextMenu, setContextMenu } = state;
@@ -159,7 +161,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
className,
)}
>
No diff available
{t("editors:noDiffAvailable")}
</div>
);
}
@@ -205,7 +207,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
options={state.options}
loading={
<div className="flex h-full items-center justify-center text-muted-foreground text-sm">
Loading diff...
{t("editors:loadingDiff")}
</div>
}
/>
@@ -25,6 +25,7 @@ import { PanelHeaderBarSplit } from "@/components/task/panel-primitives";
import { LspStatusButton } from "@/components/editors/lsp-status-button";
import type { LspStatus } from "@/lib/lsp/lsp-client-manager";
import type { LspProgressSnapshot } from "@/lib/lsp/lsp-progress";
import { useTranslation } from "react-i18next";
const SAVE_SHORTCUT =
typeof navigator !== "undefined" && navigator.platform.includes("Mac") ? "\u2318" : "Ctrl";
@@ -38,6 +39,7 @@ function SaveButton({
isSaving: boolean;
onSave: () => void;
}) {
const { t } = useTranslation();
return (
<Button
size="sm"
@@ -49,12 +51,12 @@ function SaveButton({
{isSaving ? (
<>
<IconLoader2 className="h-4 w-4 animate-spin" />
Saving...
{t("editors:saving")}
</>
) : (
<>
<IconDeviceFloppy className="h-4 w-4" />
Save
{t("common:save")}
<span className="text-xs text-muted-foreground">({SAVE_SHORTCUT}+S)</span>
</>
)}
@@ -96,14 +98,13 @@ function CommentCountBadge({
sessionId?: string;
commentCount: number;
}) {
const { t } = useTranslation();
if (!enableComments || !sessionId || commentCount <= 0) return null;
return (
<div className="flex items-center gap-1 px-2 py-1 text-xs text-primary">
<IconMessagePlus className="h-3.5 w-3.5" />
<span>
{commentCount} comment{commentCount > 1 ? "s" : ""}
</span>
<span>{t("editors:commentCount", { count: commentCount })}</span>
</div>
);
}
@@ -115,6 +116,7 @@ function DiffIndicatorsButton({
isVisible: boolean;
onToggle: () => void;
}) {
const { t } = useTranslation();
const buttonClass = `h-8 w-8 p-0 cursor-pointer ${isVisible ? "text-foreground" : "text-muted-foreground"}`;
return (
<Tooltip>
@@ -123,7 +125,9 @@ function DiffIndicatorsButton({
<IconArrowsDiff className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{isVisible ? "Hide diff indicators" : "Show diff indicators"}</TooltipContent>
<TooltipContent>
{isVisible ? t("editors:hideDiffIndicators") : t("editors:showDiffIndicators")}
</TooltipContent>
</Tooltip>
);
}
@@ -135,13 +139,14 @@ function WrapButton({
wrapEnabled: boolean;
onToggleWrap: () => void;
}) {
const { t } = useTranslation();
const wrapClass = `h-8 w-8 p-0 cursor-pointer ${wrapEnabled ? "text-foreground" : "text-muted-foreground"}`;
const wrapIcon = wrapEnabled ? (
<IconTextWrap className="h-4 w-4" />
) : (
<IconTextWrapDisabled className="h-4 w-4" />
);
const wrapLabel = wrapEnabled ? "Disable word wrap" : "Enable word wrap";
const wrapLabel = wrapEnabled ? t("editors:disableWordWrap") : t("editors:enableWordWrap");
return (
<Tooltip>
@@ -162,6 +167,7 @@ function ReloadFromAgentButton({
hasRemoteUpdate?: boolean;
onReloadFromAgent?: () => void;
}) {
const { t } = useTranslation();
if (!hasRemoteUpdate || !onReloadFromAgent) return null;
return (
@@ -174,15 +180,16 @@ function ReloadFromAgentButton({
onClick={onReloadFromAgent}
>
<IconRefresh className="h-3.5 w-3.5" />
Reload
{t("editors:reload")}
</Button>
</TooltipTrigger>
<TooltipContent>Apply latest agent changes to this file</TooltipContent>
<TooltipContent>{t("editors:applyLatestAgentChangesToFile")}</TooltipContent>
</Tooltip>
);
}
function DeleteButton({ onDelete }: { onDelete?: () => void }) {
const { t } = useTranslation();
if (!onDelete) return null;
return (
@@ -197,12 +204,13 @@ function DeleteButton({ onDelete }: { onDelete?: () => void }) {
<IconTrash className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete file</TooltipContent>
<TooltipContent>{t("editors:deleteFile")}</TooltipContent>
</Tooltip>
);
}
function MarkdownPreviewButton({ onTogglePreview }: { onTogglePreview: () => void }) {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -216,7 +224,7 @@ function MarkdownPreviewButton({ onTogglePreview }: { onTogglePreview: () => voi
<IconEye className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Preview markdown</TooltipContent>
<TooltipContent>{t("editors:previewMarkdown")}</TooltipContent>
</Tooltip>
);
}
@@ -11,6 +11,7 @@ import { useCommandPanelOpen } from "@/lib/commands/command-registry";
import { useGutterComments } from "@/hooks/use-gutter-comments";
import { consumePendingCursorPosition } from "@/hooks/use-file-editors";
import type { DiffComment } from "@/lib/diff/types";
import { useTranslation } from "react-i18next";
export type FormZoneRange = {
startLine: number;
@@ -44,6 +45,7 @@ type UseMonacoEditorStateOpts = {
// eslint-disable-next-line max-lines-per-function
export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
const { t } = useTranslation();
const { path, repo, enableComments, sessionId, wrapperRef, onChange, onSave, contentRef } = opts;
const [wrapEnabled, setWrapEnabled] = useState(true);
@@ -377,8 +379,8 @@ export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
const comment = createCommentFromForm(annotation);
if (comment) {
toast({
title: "Comment added",
description: "Your comment will be sent with your next message.",
title: t("editors:commentAdded"),
description: t("editors:commentWillBeSentWithNextMessage"),
});
}
},
@@ -392,13 +394,13 @@ export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
try {
const { queued } = await runComment(comment);
toast({
title: "Comment sent",
description: queued ? "Queued for the agent." : "Sent to the agent.",
title: t("editors:commentSent"),
description: queued ? t("editors:queuedForTheAgent") : t("editors:sentToTheAgent"),
});
} catch {
toast({
title: "Failed to send comment",
description: "Please try again.",
title: t("editors:failedToSendComment"),
description: t("editors:pleaseTryAgain"),
variant: "error",
});
}
@@ -412,13 +414,13 @@ export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
try {
const { queued } = await runComment(comment);
toast({
title: "Comment sent",
description: queued ? "Queued for the agent." : "Sent to the agent.",
title: t("editors:commentSent"),
description: queued ? t("editors:queuedForTheAgent") : t("editors:sentToTheAgent"),
});
} catch {
toast({
title: "Failed to send comment",
description: "Please try again.",
title: t("editors:failedToSendComment"),
description: t("editors:pleaseTryAgain"),
variant: "error",
});
}
@@ -430,7 +432,7 @@ export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
(commentId: string) => {
if (!sessionId) return;
removeComment(commentId);
toast({ title: "Comment deleted" });
toast({ title: t("editors:commentDeleted") });
},
[sessionId, removeComment, toast],
);
@@ -439,7 +441,7 @@ export function useMonacoEditorComments(opts: UseMonacoEditorStateOpts) {
(commentId: string, annotation: string) => {
updateComment(commentId, { text: annotation });
setEditingComment(null);
toast({ title: "Comment updated" });
toast({ title: t("editors:commentUpdated") });
},
[updateComment, setEditingComment, toast],
);
@@ -6,6 +6,7 @@ import { IconCheck, IconCopy } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { highlightCode } from "./shiki-highlighter";
import { useTranslation } from "react-i18next";
type ShikiCodeBlockProps = {
children: React.ReactNode;
@@ -13,6 +14,7 @@ type ShikiCodeBlockProps = {
};
export function ShikiCodeBlock({ children, className }: ShikiCodeBlockProps) {
const { t } = useTranslation();
const { copied, copy } = useCopyToClipboard();
const { theme, systemTheme } = useTheme();
const effectiveTheme = theme === "system" ? systemTheme : theme;
@@ -42,7 +44,7 @@ export function ShikiCodeBlock({ children, className }: ShikiCodeBlockProps) {
"opacity-0 group-hover/code-block:opacity-100",
"cursor-pointer",
)}
title="Copy code"
title={t("editors:copyCode")}
>
{copied ? (
<IconCheck className="h-3 w-3 text-green-400" />
@@ -16,6 +16,7 @@ import {
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import type { TextSelection } from "./tiptap-plan-editor";
import { useTranslation } from "react-i18next";
type PlanBubbleMenuProps = {
editor: Editor;
@@ -59,6 +60,7 @@ function LinkInput({
onSubmit: (url: string) => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
const [value, setValue] = useState("");
return (
<div className="flex items-center gap-1 bg-popover border border-border/50 rounded-lg shadow-lg p-1">
@@ -70,7 +72,7 @@ function LinkInput({
if (e.key === "Enter") onSubmit(value);
if (e.key === "Escape") onCancel();
}}
placeholder="Paste link..."
placeholder={t("editors:pasteLink")}
className="text-xs bg-transparent outline-none px-2 py-1 w-48"
autoFocus
/>
@@ -79,7 +81,7 @@ function LinkInput({
className="text-xs px-2 py-1 rounded hover:bg-muted cursor-pointer"
onClick={() => onSubmit(value)}
>
Apply
{t("editors:apply")}
</button>
</div>
);
@@ -94,48 +96,49 @@ function FormatToolbar({
onLinkClick: () => void;
onComment?: () => void;
}) {
const { t } = useTranslation();
return (
<div className="flex items-center gap-0.5 bg-popover border border-border/50 rounded-lg shadow-lg p-1">
<ToggleButton
icon={IconBold}
title="Bold (Cmd+B)"
title={t("editors:boldCmdB")}
isActive={editor.isActive("bold")}
onClick={() => editor.chain().focus().toggleBold().run()}
/>
<ToggleButton
icon={IconItalic}
title="Italic (Cmd+I)"
title={t("editors:italicCmdI")}
isActive={editor.isActive("italic")}
onClick={() => editor.chain().focus().toggleItalic().run()}
/>
<ToggleButton
icon={IconUnderline}
title="Underline (Cmd+U)"
title={t("editors:underlineCmdU")}
isActive={editor.isActive("underline")}
onClick={() => editor.chain().focus().toggleUnderline().run()}
/>
<ToggleButton
icon={IconStrikethrough}
title="Strikethrough (Cmd+Shift+X)"
title={t("editors:strikethroughCmdShiftX")}
isActive={editor.isActive("strike")}
onClick={() => editor.chain().focus().toggleStrike().run()}
/>
<MenuSeparator />
<ToggleButton
icon={IconCode}
title="Inline code"
title={t("editors:inlineCode")}
isActive={editor.isActive("code")}
onClick={() => editor.chain().focus().toggleCode().run()}
/>
<ToggleButton
icon={IconHighlight}
title="Highlight"
title={t("editors:highlight")}
isActive={editor.isActive("highlight")}
onClick={() => editor.chain().focus().toggleHighlight().run()}
/>
<ToggleButton
icon={IconLink}
title="Link"
title={t("editors:link")}
isActive={editor.isActive("link")}
onClick={onLinkClick}
/>
@@ -144,7 +147,7 @@ function FormatToolbar({
<MenuSeparator />
<ToggleButton
icon={IconMessage}
title="Comment (Cmd+Shift+C)"
title={t("editors:commentCmdShiftC")}
isActive={false}
onClick={onComment}
accent
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import type { TFunction } from "i18next";
import { buildPlanSlashCommands, filterPlanSlashCommands } from "./plan-slash-commands";
/**
* Stands in for i18next: returns the key itself under `en`, and a marked form
* under `xx`, so a locale switch is observable without loading real catalogs.
*/
function fakeT(locale: "en" | "xx"): TFunction {
const labels: Record<string, string> = {
"editors:slashHeading1": "Heading 1",
"editors:slashBulletList": "Bullet List",
"editors:slashCodeBlock": "Code Block",
"editors:slashTable": "Table",
};
return ((key: string) => {
const en = labels[key] ?? key;
return locale === "en" ? en : `[${en}]`;
}) as unknown as TFunction;
}
describe("buildPlanSlashCommands", () => {
it("resolves labels through the passed t", () => {
const commands = buildPlanSlashCommands(fakeT("en"));
expect(commands.find((c) => c.id === "heading1")?.label).toBe("Heading 1");
expect(commands.find((c) => c.id === "table")?.label).toBe("Table");
});
it("re-resolves copy when called again with a different locale", () => {
const before = buildPlanSlashCommands(fakeT("en"));
const after = buildPlanSlashCommands(fakeT("xx"));
expect(before.find((c) => c.id === "heading1")?.label).toBe("Heading 1");
expect(after.find((c) => c.id === "heading1")?.label).toBe("[Heading 1]");
});
it("keeps ids and category grouping keys stable across locales", () => {
const before = buildPlanSlashCommands(fakeT("en"));
const after = buildPlanSlashCommands(fakeT("xx"));
expect(after.map((c) => c.id)).toEqual(before.map((c) => c.id));
expect(after.map((c) => c.category)).toEqual(before.map((c) => c.category));
expect(new Set(after.map((c) => c.category))).toEqual(new Set(["text", "lists", "blocks"]));
});
});
describe("filterPlanSlashCommands", () => {
it("returns every command for an empty query", () => {
const commands = buildPlanSlashCommands(fakeT("en"));
expect(filterPlanSlashCommands(commands, "")).toHaveLength(commands.length);
});
it("matches on the translated label", () => {
const commands = buildPlanSlashCommands(fakeT("en"));
const matched = filterPlanSlashCommands(commands, "bullet");
expect(matched.map((c) => c.id)).toEqual(["bulletList"]);
});
it("matches on the stable id even when the label is translated away", () => {
const commands = buildPlanSlashCommands(fakeT("xx"));
// The label is now "[Code Block]", so this only matches via `id`.
const matched = filterPlanSlashCommands(commands, "codeblock");
expect(matched.map((c) => c.id)).toEqual(["codeBlock"]);
});
it("is case-insensitive", () => {
const commands = buildPlanSlashCommands(fakeT("en"));
expect(filterPlanSlashCommands(commands, "TABLE").map((c) => c.id)).toEqual(["table"]);
});
it("returns nothing when no command matches", () => {
const commands = buildPlanSlashCommands(fakeT("en"));
expect(filterPlanSlashCommands(commands, "nonexistent")).toEqual([]);
});
});
@@ -17,110 +17,120 @@ import {
IconMinus,
} from "@tabler/icons-react";
import type { MenuState } from "@/components/task/chat/tiptap-suggestion";
import type { TFunction } from "i18next";
// ── Types ────────────────────────────────────────────────────────────
/**
* Stable grouping id, not display copy: it is the `Map` key the menu groups by.
* `plan-slash-menu.tsx` resolves it to a translated heading at render.
*/
export type PlanSlashCategory = "text" | "lists" | "blocks";
export type PlanSlashCommand = {
id: string;
label: string;
description: string;
icon: Icon;
category: string;
category: PlanSlashCategory;
action: (editor: Editor, range: Range) => void;
};
// ── Command definitions ──────────────────────────────────────────────
const PLAN_SLASH_COMMANDS: PlanSlashCommand[] = [
// Text
const textCommands = (t: TFunction): PlanSlashCommand[] => [
{
id: "heading1",
label: "Heading 1",
description: "Large heading",
label: t("editors:slashHeading1"),
description: t("editors:slashHeading1Description"),
icon: IconH1,
category: "Text",
category: "text",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleHeading({ level: 1 }).run();
},
},
{
id: "heading2",
label: "Heading 2",
description: "Medium heading",
label: t("editors:slashHeading2"),
description: t("editors:slashHeading2Description"),
icon: IconH2,
category: "Text",
category: "text",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleHeading({ level: 2 }).run();
},
},
{
id: "heading3",
label: "Heading 3",
description: "Small heading",
label: t("editors:slashHeading3"),
description: t("editors:slashHeading3Description"),
icon: IconH3,
category: "Text",
category: "text",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleHeading({ level: 3 }).run();
},
},
// Lists
];
const listCommands = (t: TFunction): PlanSlashCommand[] => [
{
id: "bulletList",
label: "Bullet List",
description: "Unordered list",
label: t("editors:slashBulletList"),
description: t("editors:slashBulletListDescription"),
icon: IconList,
category: "Lists",
category: "lists",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
id: "numberedList",
label: "Numbered List",
description: "Ordered list",
label: t("editors:slashNumberedList"),
description: t("editors:slashNumberedListDescription"),
icon: IconListNumbers,
category: "Lists",
category: "lists",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
id: "taskList",
label: "Task List",
description: "Checklist items",
label: t("editors:slashTaskList"),
description: t("editors:slashTaskListDescription"),
icon: IconListCheck,
category: "Lists",
category: "lists",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
// Blocks
];
const blockCommands = (t: TFunction): PlanSlashCommand[] => [
{
id: "codeBlock",
label: "Code Block",
description: "Fenced code",
label: t("editors:slashCodeBlock"),
description: t("editors:slashCodeBlockDescription"),
icon: IconCode,
category: "Blocks",
category: "blocks",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
},
},
{
id: "blockquote",
label: "Blockquote",
description: "Quote text",
label: t("editors:slashBlockquote"),
description: t("editors:slashBlockquoteDescription"),
icon: IconQuote,
category: "Blocks",
category: "blocks",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
},
},
{
id: "table",
label: "Table",
description: "3x3 table",
label: t("editors:slashTable"),
description: t("editors:slashTableDescription"),
icon: IconTable,
category: "Blocks",
category: "blocks",
action: (editor, range) => {
editor
.chain()
@@ -132,16 +142,37 @@ const PLAN_SLASH_COMMANDS: PlanSlashCommand[] = [
},
{
id: "horizontalRule",
label: "Divider",
description: "Horizontal line",
label: t("editors:slashDivider"),
description: t("editors:slashDividerDescription"),
icon: IconMinus,
category: "Blocks",
category: "blocks",
action: (editor, range) => {
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
];
/** Exported for unit testing — see `plan-slash-commands.test.ts`. */
export const buildPlanSlashCommands = (t: TFunction): PlanSlashCommand[] => [
...textCommands(t),
...listCommands(t),
...blockCommands(t),
];
/**
* Matches on the translated label and on the stable `id`, so the English command
* name keeps working as a query under a non-English locale.
*
* Exported for unit testing — see `plan-slash-commands.test.ts`.
*/
export function filterPlanSlashCommands(commands: PlanSlashCommand[], query: string) {
if (!query) return commands;
const lq = query.toLowerCase();
return commands.filter(
(cmd) => cmd.label.toLowerCase().includes(lq) || cmd.id.toLowerCase().includes(lq),
);
}
// ── Empty state ──────────────────────────────────────────────────────
const EMPTY_STATE: MenuState<PlanSlashCommand> = {
@@ -157,6 +188,7 @@ const EMPTY_STATE: MenuState<PlanSlashCommand> = {
const PlanSlashPluginKey = new PluginKey("planSlashCommands");
export function createPlanSlashExtension(
getT: () => TFunction,
setMenuState: (state: MenuState<PlanSlashCommand>) => void,
onKeyDown: (event: KeyboardEvent) => boolean,
) {
@@ -172,13 +204,10 @@ export function createPlanSlashExtension(
allowSpaces: false,
startOfLine: true,
items: ({ query }) => {
if (!query) return PLAN_SLASH_COMMANDS;
const lq = query.toLowerCase();
return PLAN_SLASH_COMMANDS.filter(
(cmd) => cmd.label.toLowerCase().includes(lq) || cmd.id.toLowerCase().includes(lq),
);
},
items: ({ query }) =>
// Resolved per call, so the menu follows a locale switch without the
// extension itself having to be rebuilt.
filterPlanSlashCommands(buildPlanSlashCommands(getT()), query),
command: ({ editor, range, props: cmd }) => {
cmd.action(editor, range);
@@ -3,7 +3,18 @@
import { createElement } from "react";
import { PopupMenu, PopupMenuItem, useMenuItemRefs } from "@/components/task/chat/popup-menu";
import type { MenuState } from "@/components/task/chat/tiptap-suggestion";
import type { PlanSlashCommand } from "./plan-slash-commands";
import type { PlanSlashCategory, PlanSlashCommand } from "./plan-slash-commands";
import { useTranslation } from "react-i18next";
/**
* Category ids are stable grouping keys, so the heading copy is resolved here at
* render rather than stored on the command. Holds keys, never `t()` results.
*/
const CATEGORY_LABEL_KEYS: Record<PlanSlashCategory, string> = {
text: "editors:slashCategoryText",
lists: "editors:slashCategoryLists",
blocks: "editors:slashCategoryBlocks",
};
type PlanSlashMenuProps = {
menuState: MenuState<PlanSlashCommand>;
@@ -12,12 +23,13 @@ type PlanSlashMenuProps = {
};
export function PlanSlashMenu({ menuState, selectedIndex, setSelectedIndex }: PlanSlashMenuProps) {
const { t } = useTranslation();
const { setItemRef } = useMenuItemRefs(selectedIndex);
if (!menuState.isOpen || menuState.items.length === 0) return null;
// Group items by category
const grouped = new Map<string, { items: PlanSlashCommand[]; startIndex: number }>();
const grouped = new Map<PlanSlashCategory, { items: PlanSlashCommand[]; startIndex: number }>();
let idx = 0;
for (const item of menuState.items) {
const existing = grouped.get(item.category);
@@ -34,7 +46,7 @@ export function PlanSlashMenu({ menuState, selectedIndex, setSelectedIndex }: Pl
isOpen={menuState.isOpen}
position={null}
clientRect={menuState.clientRect}
title="Insert block"
title={t("editors:insertBlock")}
selectedIndex={selectedIndex}
onClose={() => menuState.command?.(menuState.items[0]!)}
hasItems={menuState.items.length > 0}
@@ -44,7 +56,7 @@ export function PlanSlashMenu({ menuState, selectedIndex, setSelectedIndex }: Pl
<div key={category}>
<div className="px-3 pt-2 pb-1">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
{category}
{t(CATEGORY_LABEL_KEYS[category])}
</span>
</div>
{group.items.map((item) => {
@@ -8,6 +8,8 @@ import type { Node as PmNode } from "@tiptap/pm/model";
import type { NodeView } from "@tiptap/pm/view";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import mermaid from "mermaid";
// Resolved inside the node view, which is a ProseMirror callback with no hook scope.
import { t } from "@/lib/i18n";
import {
DEFAULT_SCALE,
SCALE_STEP,
@@ -180,7 +182,7 @@ class MermaidNodeView implements NodeView {
codeToggle.type = "button";
codeToggle.className = "mermaid-zoom-btn";
codeToggle.innerHTML = CODE_ICON_SVG;
codeToggle.title = "Toggle code";
codeToggle.title = t("editors:toggleCode");
codeToggle.addEventListener("click", () => this.toggleCode());
this.toolbar.append(
@@ -32,6 +32,7 @@ import { PlanDragHandle } from "./plan-drag-handle";
import type { MenuState } from "@/components/task/chat/tiptap-suggestion";
import { DOMParser as PmDOMParser } from "@tiptap/pm/model";
import type { Editor } from "@tiptap/core";
import { useTranslation } from "react-i18next";
export type { CommentForEditor };
@@ -196,6 +197,7 @@ function handleSlashKeyDown(
/** Slash command menu state and keyboard navigation. */
function useSlashMenu() {
const { t } = useTranslation();
const [menuState, setMenuState] = useState<MenuState<PlanSlashCommand>>(EMPTY_SLASH_STATE);
const [selectedIndex, setSelectedIndex] = useState(0);
const selectedIndexRef = useRef(0);
@@ -221,10 +223,19 @@ function useSlashMenu() {
// Stable callback passed to extension — never changes, delegates via ref
const stableKeyDown = useCallback((event: KeyboardEvent) => keyDownRef.current(event), []);
// Same ref treatment for `t`: keeping it out of the extension's identity stops
// a locale switch from rebuilding the ProseMirror plugin mid-edit, and the
// command list is rebuilt per `items()` call, so the menu still re-resolves.
const tRef = useRef(t);
useLayoutEffect(() => {
tRef.current = t;
});
const stableGetT = useCallback(() => tRef.current, []);
/* eslint-disable react-hooks/refs -- stableKeyDown reads ref for deferred access, not during render */
const extension = useMemo(
() => createPlanSlashExtension(setMenuState, stableKeyDown),
[stableKeyDown],
() => createPlanSlashExtension(stableGetT, setMenuState, stableKeyDown),
[stableGetT, stableKeyDown],
);
/* eslint-enable react-hooks/refs */
@@ -283,16 +294,17 @@ type PlanEditorState = {
/** Hook encapsulating TipTap editor setup, extensions, and lifecycle effects. */
function usePlanEditor(props: TipTapPlanEditorProps): PlanEditorState {
const { t } = useTranslation();
const {
value,
onChange,
placeholder = "Start typing...",
onSelectionChange,
comments = [],
onCommentClick,
onCommentDeleted,
onEditorReady,
} = props;
const placeholder = props.placeholder ?? t("editors:startTyping");
const editorRef = useRef<ReturnType<typeof useEditor>>(null);
const onChangeRef = useRef(onChange);
@@ -370,6 +382,7 @@ function usePlanEditor(props: TipTapPlanEditorProps): PlanEditorState {
}
export function TipTapPlanEditor(props: TipTapPlanEditorProps) {
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
const wrapperRef = useRef<HTMLDivElement>(null);
const { editor, editorRef, onSelectionChangeRef, onCommentClickRef, isReady, slash } =
@@ -406,7 +419,7 @@ export function TipTapPlanEditor(props: TipTapPlanEditorProps) {
/>
{!isReady && (
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-background/80">
Loading editor...
{t("editors:loadingEditor")}
</div>
)}
</div>
@@ -1,6 +1,7 @@
"use client";
import { Switch } from "@kandev/ui/switch";
import { useTranslation } from "react-i18next";
export function ConfigurationChatToggle({
checked,
@@ -11,6 +12,7 @@ export function ConfigurationChatToggle({
disabled?: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
const { t } = useTranslation();
return (
<section
className="flex min-h-11 items-center justify-between gap-4"
@@ -18,14 +20,12 @@ export function ConfigurationChatToggle({
>
<div className="min-w-0">
<h3 id="config-chat-mode-label" className="text-sm font-medium">
Configuration chat
{t("chat:configurationChat")}
</h3>
<p className="text-xs text-muted-foreground">
Let the agent update Kandev settings, workflows, agent profiles, and MCP configuration.
</p>
<p className="text-xs text-muted-foreground">{t("chat:configurationChatDescription")}</p>
</div>
<Switch
aria-label="Configuration chat"
aria-label={t("chat:configurationChat")}
checked={checked}
disabled={disabled}
onCheckedChange={onCheckedChange}
@@ -18,6 +18,7 @@ import { useResizableClarificationOverlay } from "@/hooks/use-resizable-clarific
import type { Message } from "@/lib/types/http";
import { getSessionWorkspacePath } from "@/lib/session-workspace-path";
import { routePanelMouseDown } from "@/components/task/chat/route-panel-mouse-down";
import { useTranslation } from "react-i18next";
type QuickChatContentProps = {
sessionId: string;
@@ -40,6 +41,7 @@ function QuickChatClarificationSection({
onResolved,
shortcutScopeRef,
}: QuickChatClarificationSectionProps) {
const { t } = useTranslation();
const [collapsed, setCollapsed] = useState(false);
const contentId = useId();
const { height, containerRef, resetHeight, resizeHandleProps } =
@@ -56,7 +58,7 @@ function QuickChatClarificationSection({
if (!pending) return null;
const actionLabel = collapsed ? "Expand clarification" : "Collapse clarification";
const actionLabel = collapsed ? t("chat:expandClarification") : t("chat:collapseClarification");
return (
<div className="relative flex-shrink-0 border-t border-sky-400/30 bg-card">
@@ -74,7 +76,7 @@ function QuickChatClarificationSection({
<div className="flex h-11 flex-shrink-0 items-center justify-between gap-2 pl-4">
<div className="flex min-w-0 items-center gap-2 text-sm font-medium">
<IconMessageQuestion className="h-4 w-4 flex-shrink-0 text-blue-500" />
<span className="truncate">Clarification needed</span>
<span className="truncate">{t("chat:clarificationNeeded")}</span>
</div>
<Button
type="button"
@@ -10,6 +10,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@kandev/ui/alert-dialog";
import { useTranslation } from "react-i18next";
type QuickChatDeleteDialogProps = {
sessionToDelete: string | null;
@@ -22,30 +23,31 @@ export function QuickChatDeleteDialog({
onOpenChange,
onConfirm,
}: QuickChatDeleteDialogProps) {
const { t } = useTranslation();
return (
<AlertDialog open={!!sessionToDelete} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Quick Chat?</AlertDialogTitle>
<AlertDialogTitle>{t("chat:deleteQuickChat")}</AlertDialogTitle>
<AlertDialogDescription asChild>
<div>
<p>This will permanently delete this quick chat session, including:</p>
<p>{t("chat:deleteQuickChatIntro")}</p>
<ul className="list-disc list-inside mt-2 space-y-1">
<li>All conversation history</li>
<li>The task and its data</li>
<li>The associated worktree</li>
<li>{t("chat:allConversationHistory")}</li>
<li>{t("chat:theTaskAndItsData")}</li>
<li>{t("chat:theAssociatedWorktree")}</li>
</ul>
<p className="mt-2">This action cannot be undone.</p>
<p className="mt-2">{t("chat:thisActionCannotBeUndone")}</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
<AlertDialogCancel className="cursor-pointer">{t("common:cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
{t("chat:delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -11,6 +11,7 @@ import { useToast } from "@/components/toast-provider";
import { startQuickChat } from "@/lib/api/domains/workspace-api";
import type { Repository } from "@/lib/types/http";
import type { AgentProfileOption } from "@/lib/state/slices/settings/types";
import { useTranslation } from "react-i18next";
type QuickChatPickerDialogProps = {
open: boolean;
@@ -30,23 +31,22 @@ type FormState = {
const NONE_VALUE = "__none__";
function QuickChatFormBody({ state }: { state: FormState }) {
const { t } = useTranslation();
const { selectedRepoId, setSelectedRepoId, selectedAgentId, setSelectedAgentId } = state;
return (
<div className="p-4 space-y-4">
<p className="text-sm text-muted-foreground">
Start a quick conversation with an agent without creating a formal task.
</p>
<p className="text-sm text-muted-foreground">{t("chat:quickChatDialogIntro")}</p>
<div className="space-y-2">
<Label htmlFor="repository">Repository (optional)</Label>
<Label htmlFor="repository">{t("chat:repositoryOptional")}</Label>
<Select
value={selectedRepoId || NONE_VALUE}
onValueChange={(v) => setSelectedRepoId(v === NONE_VALUE ? "" : v)}
>
<SelectTrigger id="repository" className="w-full">
<SelectValue placeholder="Select a repository..." />
<SelectValue placeholder={t("chat:selectARepository")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_VALUE}>No repository</SelectItem>
<SelectItem value={NONE_VALUE}>{t("chat:noRepository")}</SelectItem>
{state.repositories.map((repo) => (
<SelectItem key={repo.id} value={repo.id}>
{repo.name}
@@ -56,16 +56,16 @@ function QuickChatFormBody({ state }: { state: FormState }) {
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="agent">Agent (optional)</Label>
<Label htmlFor="agent">{t("chat:agentOptional")}</Label>
<Select
value={selectedAgentId || NONE_VALUE}
onValueChange={(v) => setSelectedAgentId(v === NONE_VALUE ? "" : v)}
>
<SelectTrigger id="agent" className="w-full">
<SelectValue placeholder="Use workspace default..." />
<SelectValue placeholder={t("chat:useWorkspaceDefault")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_VALUE}>Use workspace default</SelectItem>
<SelectItem value={NONE_VALUE}>{t("chat:useWorkspaceDefaultOption")}</SelectItem>
{state.agentProfiles.map((profile) => (
<SelectItem key={profile.id} value={profile.id}>
{profile.label}
@@ -84,6 +84,7 @@ export const QuickChatPickerDialog = memo(function QuickChatPickerDialog({
onOpenChange,
workspaceId,
}: QuickChatPickerDialogProps) {
const { t } = useTranslation();
const { toast } = useToast();
const openQuickChat = useAppStore((s) => s.openQuickChat);
const [isStarting, setIsStarting] = useState(false);
@@ -105,8 +106,8 @@ export const QuickChatPickerDialog = memo(function QuickChatPickerDialog({
openQuickChat(response.session_id, workspaceId, undefined, "chat", response.task_id);
} catch (error) {
toast({
title: "Failed to start quick chat",
description: error instanceof Error ? error.message : "Unknown error",
title: t("chat:failedToStartQuickChat"),
description: error instanceof Error ? error.message : t("chat:unknownError"),
variant: "error",
});
} finally {
@@ -138,9 +139,9 @@ export const QuickChatPickerDialog = memo(function QuickChatPickerDialog({
showCloseButton={false}
overlayClassName="bg-black/20"
>
<DialogTitle className="sr-only">New Quick Chat</DialogTitle>
<DialogTitle className="sr-only">{t("chat:newQuickChat")}</DialogTitle>
<div className="flex items-center justify-between px-4 py-3 border-b">
<h2 className="text-lg font-semibold">New Quick Chat</h2>
<h2 className="text-lg font-semibold">{t("chat:newQuickChat")}</h2>
<Button
variant="ghost"
size="icon"
@@ -153,11 +154,11 @@ export const QuickChatPickerDialog = memo(function QuickChatPickerDialog({
<QuickChatFormBody state={formState} />
<div className="flex justify-end gap-2 px-4 py-3 border-t bg-muted/30">
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
Cancel
{t("common:cancel")}
</Button>
<Button onClick={handleStart} disabled={isStarting} className="cursor-pointer">
<IconRocket className="h-4 w-4 mr-2" />
{isStarting ? "Starting..." : "Start Chat"}
{isStarting ? t("chat:starting") : t("chat:startChat")}
</Button>
</div>
</DialogContent>
@@ -15,14 +15,18 @@ import { ConfigChatSetup } from "@/components/config-chat/config-chat-setup";
import { useConfigChat } from "@/components/config-chat/use-config-chat";
import type { QuickChatSession } from "@/lib/state/slices/ui/types";
import { isQuickChatSetupSessionId } from "@/lib/state/slices/ui/quick-chat-session";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type QuickChatModalProps = {
workspaceId: string;
};
function quickChatTabName(session: QuickChatSession, index: number) {
if (!isQuickChatSetupSessionId(session.sessionId)) return session.name || `Chat ${index + 1}`;
return session.kind === "config" ? "Configuration Chat" : "New Chat";
function quickChatTabName(t: TFunction, session: QuickChatSession, index: number) {
if (!isQuickChatSetupSessionId(session.sessionId)) {
return session.name || t("chat:chatTabName", { index: index + 1 });
}
return session.kind === "config" ? t("chat:configurationChatTab") : t("chat:newChatTab");
}
function QuickChatTabs({
@@ -42,13 +46,14 @@ function QuickChatTabs({
onRename: (sessionId: string, name: string) => void;
onCloseModal: () => void;
}) {
const { t } = useTranslation();
if (sessions.length === 0) return null;
return (
<div className="flex items-center gap-1 px-2 py-1 border-b bg-muted/20">
<div className="flex items-center gap-1 overflow-x-auto flex-1 scrollbar-hide">
{sessions.map((s, index) => {
const tabName = quickChatTabName(s, index);
const tabName = quickChatTabName(t, s, index);
return (
<QuickChatTabItem
key={s.sessionId || `new-${index}`}
@@ -67,7 +72,7 @@ function QuickChatTabs({
variant="ghost"
className="h-11 w-11 shrink-0 cursor-pointer sm:h-6 sm:w-6"
onClick={onNewChat}
aria-label="Start new chat"
aria-label={t("chat:startNewChat")}
>
<IconPlus className="h-4 w-4 sm:h-3.5 sm:w-3.5" />
</Button>
@@ -79,7 +84,7 @@ function QuickChatTabs({
variant="ghost"
className="h-11 w-11 shrink-0 cursor-pointer p-0 sm:hidden"
onClick={onCloseModal}
aria-label="Close quick chat"
aria-label={t("chat:closeQuickChat")}
data-testid="quick-chat-close"
>
<IconX className="h-3.5 w-3.5" />
@@ -95,11 +100,14 @@ function QuickChatResizeHandle({
edge: "left" | "right";
onMouseDown: (event: React.MouseEvent) => void;
}) {
const { t } = useTranslation();
return (
<button
type="button"
tabIndex={-1}
aria-label={`Resize quick chat from ${edge}`}
aria-label={
edge === "left" ? t("chat:resizeQuickChatFromLeft") : t("chat:resizeQuickChatFromRight")
}
data-testid={`quick-chat-resize-${edge}`}
onMouseDown={onMouseDown}
className={`group absolute inset-y-0 z-20 hidden w-2 cursor-ew-resize items-center justify-center sm:flex ${
@@ -116,6 +124,7 @@ function QuickChatResizeHandle({
}
export const QuickChatModal = memo(function QuickChatModal({ workspaceId }: QuickChatModalProps) {
const { t } = useTranslation();
const configChat = useConfigChat(workspaceId);
const {
isOpen,
@@ -150,7 +159,7 @@ export const QuickChatModal = memo(function QuickChatModal({ workspaceId }: Quic
showCloseButton={false}
overlayClassName="bg-black/20"
>
<DialogTitle className="sr-only">Quick Chat</DialogTitle>
<DialogTitle className="sr-only">{t("common:commandQuickChat")}</DialogTitle>
<QuickChatResizeHandle edge="left" {...leftResizeHandleProps} />
<QuickChatResizeHandle edge="right" {...rightResizeHandleProps} />
<QuickChatTabs
@@ -5,6 +5,7 @@ import { useEnsureTaskSession } from "@/hooks/use-ensure-task-session";
import { PassthroughTerminal } from "@/components/task/passthrough-terminal";
import type { QuickChatSession } from "@/lib/state/slices/ui/types";
import { QuickChatContent } from "./quick-chat-content";
import { useTranslation } from "react-i18next";
function useIsQuickChatPassthrough(sessionId: string) {
return useAppStore((state) => {
@@ -24,6 +25,7 @@ type QuickChatSessionViewProps = {
};
export function QuickChatSessionView({ session, onInitialPromptSent }: QuickChatSessionViewProps) {
const { t } = useTranslation();
// A tab can arrive from a task event, which carries no session payload.
// Fetch the row on open so such a tab is usable, not just visible.
useEnsureTaskSession(session.sessionId);
@@ -40,7 +42,7 @@ export function QuickChatSessionView({ session, onInitialPromptSent }: QuickChat
<QuickChatContent
sessionId={session.sessionId}
minimalToolbar={isConfig}
placeholderOverride={isConfig ? "Ask anything about your configuration..." : undefined}
placeholderOverride={isConfig ? t("chat:configChatPlaceholder") : undefined}
initialPrompt={session.initialPrompt}
onInitialPromptSent={onInitialPromptSent}
/>
@@ -16,6 +16,8 @@ import { isSelectableAgentProfile } from "@/lib/state/slices/settings/types";
import type { Repository } from "@/lib/types/http";
import type { QuickChatSessionKind } from "@/lib/state/slices/ui/types";
import { ConfigurationChatToggle } from "./configuration-chat-toggle";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type QuickChatSetupProps = {
workspaceId: string;
@@ -26,28 +28,42 @@ type QuickChatSetupProps = {
onKindChange: (kind: QuickChatSessionKind) => void;
};
function repositoryAddState(isLoading: boolean, repositoryCount: number, rowCount: number) {
if (isLoading) return { canAddMore: false, addHint: "Loading repositories" };
function repositoryAddState(
t: TFunction,
isLoading: boolean,
repositoryCount: number,
rowCount: number,
) {
if (isLoading) return { canAddMore: false, addHint: t("chat:loadingRepositories") };
if (repositoryCount === 0) {
return { canAddMore: false, addHint: "No repositories available in this workspace" };
return { canAddMore: false, addHint: t("chat:noRepositoriesAvailableInWorkspace") };
}
if (rowCount >= repositoryCount) {
return { canAddMore: false, addHint: "All workspace repositories are already added" };
return { canAddMore: false, addHint: t("chat:allWorkspaceRepositoriesAdded") };
}
return { canAddMore: true, addHint: undefined };
}
function QuickChatIntroduction() {
const { t } = useTranslation();
return (
<div className="space-y-1" data-testid="quick-chat-introduction">
<p className="text-sm text-foreground">
Chat with an agent about an idea, question, or codebase.
</p>
<p className="text-sm text-muted-foreground">Quick chats stay outside your task board.</p>
<p className="text-sm text-foreground">{t("chat:quickChatIntro")}</p>
<p className="text-sm text-muted-foreground">{t("chat:quickChatIntroSecondary")}</p>
</div>
);
}
function QuickChatSetupHeader() {
const { t } = useTranslation();
return (
<header className="space-y-1">
<h2 className="text-lg font-semibold">{t("common:commandQuickChat")}</h2>
<QuickChatIntroduction />
</header>
);
}
function AgentField({
profiles,
value,
@@ -59,15 +75,16 @@ function AgentField({
disabled: boolean;
onChange: (value: string) => void;
}) {
const { t } = useTranslation();
const options = useAgentProfileOptions(profiles);
return (
<section className="space-y-2" aria-labelledby="quick-chat-agent-label">
<div>
<h3 id="quick-chat-agent-label" className="text-sm font-medium">
Agent profile
{t("chat:agentProfile")}
</h3>
<p id="quick-chat-agent-help" className="text-xs text-muted-foreground">
Choose the agent for this conversation.
{t("chat:agentProfileHelp")}
</p>
</div>
<AgentSelector
@@ -75,7 +92,7 @@ function AgentField({
value={value}
onValueChange={onChange}
disabled={disabled}
placeholder={profiles.length > 0 ? "Select agent" : "No agents available"}
placeholder={profiles.length > 0 ? t("chat:selectAgent") : t("chat:noAgentsAvailable")}
triggerClassName="h-11 w-full justify-between border border-input bg-background px-3 shadow-xs hover:bg-accent/50 data-[state=open]:border-ring data-[state=open]:ring-[2px] data-[state=open]:ring-ring/35"
popoverPortal
/>
@@ -96,15 +113,17 @@ type RepositoryFieldProps = {
};
function RepositoryField(props: RepositoryFieldProps) {
const { t } = useTranslation();
return (
<section className="space-y-3" aria-labelledby="quick-chat-repositories-label">
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
<h3 id="quick-chat-repositories-label" className="text-sm font-medium">
Repositories <span className="font-normal text-muted-foreground">(optional)</span>
{t("chat:repositories")}{" "}
<span className="font-normal text-muted-foreground">{t("chat:optional")}</span>
</h3>
<p id="quick-chat-repositories-help" className="text-xs text-muted-foreground">
Add repository context to focus on specific code and branches.
{t("chat:repositoriesHelp")}
</p>
</div>
<RepositoryContextHelp />
@@ -119,7 +138,7 @@ function RepositoryField(props: RepositoryFieldProps) {
workspaceId={props.workspaceId}
canAddMore={props.canAddMore}
addHint={props.addHint}
addLabel="Add repository"
addLabel={t("chat:addRepository")}
allowDuplicateRepositories={false}
onAdd={props.onAdd}
onRemove={props.onRemove}
@@ -132,21 +151,19 @@ function RepositoryField(props: RepositoryFieldProps) {
}
function RepositoryContextHelp() {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="About repository context"
aria-label={t("chat:aboutRepositoryContext")}
className="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
>
<IconInfoCircle className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Kandev uses an isolated worktree from the selected branch. Uncommitted local changes are not
included.
</TooltipContent>
<TooltipContent className="max-w-xs">{t("chat:repositoryContextHelp")}</TooltipContent>
</Tooltip>
);
}
@@ -162,13 +179,14 @@ function SetupFooter({
onCancel: () => void;
onStart: () => void;
}) {
const { t } = useTranslation();
return (
<footer
className="flex shrink-0 items-center justify-end gap-2 border-t bg-popover px-4 py-3 sm:px-8"
data-testid="quick-chat-setup-footer"
>
<Button variant="outline" onClick={onCancel} disabled={isStarting} className="cursor-pointer">
Cancel
{t("common:cancel")}
</Button>
<Button
onClick={onStart}
@@ -178,7 +196,7 @@ function SetupFooter({
data-dialog-default-action
>
{isStarting ? <IconLoader2 className="h-4 w-4 animate-spin" /> : null}
{isStarting ? "Starting chat..." : "Start chat"}
{isStarting ? t("chat:startingChat") : t("chat:startChatAction")}
</Button>
</footer>
);
@@ -192,6 +210,7 @@ export function QuickChatSetup({
onCancel,
onKindChange,
}: QuickChatSetupProps) {
const { t } = useTranslation();
const agentProfiles = useAppStore((state) => state.agentProfiles.items ?? []);
const defaultAgentId = useAppStore(
(state) =>
@@ -219,6 +238,7 @@ export function QuickChatSetup({
const isStarting = pendingAgentId !== null;
const hasIncompleteRow = repoState.repositories.some((row) => !row.repositoryId || !row.branch);
const { canAddMore, addHint } = repositoryAddState(
t,
isLoading,
repositories.length,
repoState.repositories.length,
@@ -245,10 +265,7 @@ export function QuickChatSetup({
<div className="flex min-h-0 flex-1 flex-col bg-popover" data-testid="quick-chat-setup">
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-6 sm:px-8 sm:py-8">
<div className="mx-auto w-full max-w-2xl space-y-7">
<header className="space-y-1">
<h2 className="text-lg font-semibold">Quick Chat</h2>
<QuickChatIntroduction />
</header>
<QuickChatSetupHeader />
{canCreateConfigurationChat && (
<ConfigurationChatToggle
checked={false}
@@ -3,6 +3,7 @@
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { IconSparkles, IconX } from "@tabler/icons-react";
import type { QuickChatSessionKind } from "@/lib/state/slices/ui/types";
import { useTranslation } from "react-i18next";
type QuickChatTabItemProps = {
name: string;
@@ -24,6 +25,7 @@ export const QuickChatTabItem = memo(function QuickChatTabItem({
onClose,
onRename,
}: QuickChatTabItemProps) {
const { t } = useTranslation();
const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState(name);
const inputRef = useRef<HTMLInputElement>(null);
@@ -75,7 +77,7 @@ export const QuickChatTabItem = memo(function QuickChatTabItem({
{isEditing ? (
<input
ref={inputRef}
aria-label="Rename chat"
aria-label={t("chat:renameChat")}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
@@ -101,11 +103,11 @@ export const QuickChatTabItem = memo(function QuickChatTabItem({
type="button"
onClick={onActivate}
onDoubleClick={handleStartEdit}
title={isRenameable ? "Double-click to rename" : undefined}
title={isRenameable ? t("chat:doubleClickToRename") : undefined}
className="flex items-center px-2.5 py-1 text-xs cursor-pointer"
>
{kind === "config" && (
<span role="img" aria-label="Configuration chat" className="mr-1.5 shrink-0">
<span role="img" aria-label={t("chat:configurationChat")} className="mr-1.5 shrink-0">
<IconSparkles className="h-3 w-3" aria-hidden />
</span>
)}
@@ -114,7 +116,7 @@ export const QuickChatTabItem = memo(function QuickChatTabItem({
)}
<button
type="button"
aria-label={`Close ${name}`}
aria-label={t("chat:close", { name })}
className="p-1 cursor-pointer opacity-60 hover:opacity-100"
onClick={onClose}
>
@@ -8,6 +8,7 @@ import { startQuickChat, type QuickChatRepositoryInput } from "@/lib/api/domains
import { isQuickChatSetupSessionId } from "@/lib/state/slices/ui/quick-chat-session";
import { persistQuickChatRename } from "@/lib/quick-chat/rename";
import type { QuickChatSessionKind } from "@/lib/state/slices/ui/types";
import { useTranslation } from "react-i18next";
const noop = () => {};
@@ -102,6 +103,7 @@ async function startQuickChatForAgent(
*
* Exported for unit testing — see `use-quick-chat-modal.test.ts`. */
export function useAgentSelection(workspaceId: string, store: QuickChatStore) {
const { t } = useTranslation();
const { toast } = useToast();
const [pendingAgentId, setPendingAgentId] = useState<string | null>(null);
// Monotonic request id; the latest click "wins" — older responses get
@@ -136,8 +138,8 @@ export function useAgentSelection(workspaceId: string, store: QuickChatStore) {
} catch (error) {
if (latestRequestId.current !== requestId) return;
toast({
title: "Failed to start quick chat",
description: error instanceof Error ? error.message : "Unknown error",
title: t("chat:failedToStartQuickChat"),
description: error instanceof Error ? error.message : t("chat:unknownError"),
variant: "error",
});
} finally {
@@ -153,6 +155,7 @@ export function useAgentSelection(workspaceId: string, store: QuickChatStore) {
}
function useQuickChatSessionClose(store: QuickChatStore, resetPendingStarts: () => void) {
const { t } = useTranslation();
const { toast } = useToast();
const [sessionToClose, setSessionToClose] = useState<string | null>(null);
const handleCloseTab = useCallback(
@@ -181,8 +184,8 @@ function useQuickChatSessionClose(store: QuickChatStore, resetPendingStarts: ()
} catch (error) {
console.error("Failed to delete quick chat task:", error);
toast({
title: "Failed to delete quick chat",
description: error instanceof Error ? error.message : "Unknown error",
title: t("chat:failedToDeleteQuickChat"),
description: error instanceof Error ? error.message : t("chat:unknownError"),
variant: "error",
});
}
@@ -191,6 +194,7 @@ function useQuickChatSessionClose(store: QuickChatStore, resetPendingStarts: ()
}
export function useQuickChatModal(workspaceId: string, onSupersedeConfigStart = noop) {
const { t } = useTranslation();
const { toast } = useToast();
const store = useQuickChatStore(workspaceId);
const { sessions, activeSession } = useWorkspaceQuickChat(store);
@@ -262,8 +266,8 @@ export function useQuickChatModal(workspaceId: string, onSupersedeConfigStart =
store.renameQuickChatSession(sessionId, name);
persistQuickChatRename(sessionId, resolveTaskId(store, sessionId), name).catch(() => {
toast({
title: "Rename saved on this device only",
description: "We could not sync the new name to your other devices.",
title: t("chat:renameSavedOnThisDeviceOnly"),
description: t("chat:renameSyncFailedDescription"),
variant: "error",
});
});
+20 -5
View File
@@ -1164,11 +1164,13 @@ export const i18nGuardFiles = [
// - `components/settings/config-chat-agent-section.tsx`, the Configuration
// Chat card on Settings → Utility Agents. #2218 migrates it with the page
// that owns it, into `settings:configChatAgent*`.
// - `components/quick-chat/**`. `quick-chat-modal.tsx` renders
// `ConfigChatSetup` in its `presentation="dialog"` form and calls
// `useConfigChat`, so it inherits everything migrated here, but its own
// chrome — and `configuration-chat-toggle.tsx`, which `ConfigChatSetup`
// renders — is still English and belongs to the quick-chat migration.
//
// `components/quick-chat/**` was the other entry here until #2300 migrated it
// — `quick-chat-modal.tsx` renders `ConfigChatSetup` in its
// `presentation="dialog"` form and calls `useConfigChat`, so it inherited
// everything migrated here, and its own chrome (plus
// `configuration-chat-toggle.tsx`, which `ConfigChatSetup` renders) now lands
// on the same `chat:` namespace. It is listed at the end of this file.
"components/config-chat/*.{ts,tsx}",
// Automations — `components/automations/**` (incl. `trigger-configs/`) and
@@ -1595,4 +1597,17 @@ export const i18nGuardFiles = [
"components/kanban-with-preview.tsx",
// Shared by the desktop dropdown and the mobile sheet; returns catalog keys.
"lib/kanban/repository-placeholder.ts",
// The editor surfaces: Monaco/CodeMirror editors and diff viewers, the Shiki
// and Monaco code blocks, the TipTap plan editor, and the file-actions menu.
// Copy lands in the `editors:` namespace.
//
// Two of these files hold copy the rule cannot see, so it is recorded here
// rather than enforced: `tiptap-mermaid-extension.ts` and the CodeMirror
// gutter marker in `use-codemirror-editor-state.ts` build DOM imperatively
// from ProseMirror/CodeMirror callbacks, which have no hook scope, so both
// resolve through the module-level `t` from `@/lib/i18n`.
"components/editors/**/*.{ts,tsx}",
// Quick chat: the modal, its tabs, the setup form and the delete dialog.
// Copy lands in the existing `chat:` namespace, shared with config chat.
"components/quick-chat/**/*.{ts,tsx}",
];
+80 -25
View File
@@ -1,32 +1,66 @@
{
"addToChatContext": "Add to chat context",
"aboutRepositoryContext": "About repository context",
"addedToChatContext": "{{name}} added to chat context",
"collapseMessage": "Collapse message",
"collapseQueuedMessages": "Collapse queued messages",
"clearAll": "Clear all",
"clearAllQueuedMessages": "Clear all queued messages",
"addRepository": "Add repository",
"addToChatContext": "Add to chat context",
"agentOptional": "Agent (optional)",
"agentProfile": "Agent profile",
"agentProfileHelp": "Choose the agent for this conversation.",
"allConversationHistory": "All conversation history",
"allWorkspaceRepositoriesAdded": "All workspace repositories are already added",
"attachmentDeliveryFile": "File",
"attachmentDeliveryMode": "Attachment delivery mode",
"attachmentDeliveryPathDescription": "Upload into the workspace so the agent can read or edit the file.",
"attachmentDeliveryPrompt": "Prompt",
"attachmentDeliveryPromptDescription": "Send as prompt context for visual understanding. The agent will not get a file path.",
"attachmentFallbackName": "Attachment",
"attachmentMessageAlt": "Attachment {{index}}",
"attachmentPreviewAlt": "Preview",
"attachmentPreviewFullAlt": "Full size preview",
"attachmentUploadFailed": "Attachment upload failed",
"attachmentUploadPending": "Uploading attachment…",
"attachmentUploadPendingSubmit": "Finish uploading attachments before sending.",
"attachmentUploadFailed": "Attachment upload failed",
"retryAttachmentUpload": "Retry attachment upload",
"attachmentPreviewAlt": "Preview",
"attachmentMessageAlt": "Attachment {{index}}",
"attachmentFallbackName": "Attachment",
"attachmentPreviewFullAlt": "Full size preview",
"attachmentDeliveryMode": "Attachment delivery mode",
"attachmentDeliveryPrompt": "Prompt",
"attachmentDeliveryFile": "File",
"attachmentDeliveryPromptDescription": "Send as prompt context for visual understanding. The agent will not get a file path.",
"attachmentDeliveryPathDescription": "Upload into the workspace so the agent can read or edit the file.",
"chatTabName": "Chat {{index}}",
"clarificationNeeded": "Clarification needed",
"clearAll": "Clear all",
"clearAllQueuedMessages": "Clear all queued messages",
"close": "Close {{name}}",
"closeQuickChat": "Close quick chat",
"collapseClarification": "Collapse clarification",
"collapseMessage": "Collapse message",
"collapseQueuedMessages": "Collapse queued messages",
"configChatPlaceholder": "Ask anything about your configuration...",
"configurationChat": "Configuration chat",
"configurationChatDescription": "Let the agent update Kandev settings, workflows, agent profiles, and MCP configuration.",
"configurationChatTab": "Configuration Chat",
"delete": "Delete",
"deleteQuickChat": "Delete Quick Chat?",
"deleteQuickChatIntro": "This will permanently delete this quick chat session, including:",
"doubleClickToRename": "Double-click to rename",
"editQueuedMessage": "Edit queued message",
"expandClarification": "Expand clarification",
"expandMessage": "Expand message",
"failedToClearQueuedMessages": "Failed to clear queued messages.",
"failedToDeleteQuickChat": "Failed to delete quick chat",
"failedToMergeQueuedMessages": "Failed to merge queued messages.",
"failedToRemoveQueuedMessage": "Failed to remove queued message.",
"failedToRunQueuedMessage": "Failed to run queued message.",
"failedToStartQuickChat": "Failed to start quick chat",
"fileTreeActions": "File actions",
"loadingRepositories": "Loading repositories",
"mergeReferenceOverflow": "Too many entity references to merge these queued messages.",
"mergeWithAbove": "Merge with above",
"newChatTab": "New Chat",
"newQuickChat": "New Quick Chat",
"noAgentsAvailable": "No agents available",
"noRepositoriesAvailableInWorkspace": "No repositories available in this workspace",
"noRepository": "No repository",
"optional": "(optional)",
"providerQuotaModel": "Model: {{model}}",
"providerQuotaProviderFallback": "Provider",
"providerQuotaReset": "Capacity resets {{resetAt}}.",
"providerQuotaResetUnknown": "Try again when the provider makes capacity available.",
"providerQuotaTitle": "{{provider}} usage limit reached",
"queueCapacity_one": "{{count}} of {{max}}",
"queueCapacity_other": "{{count}} of {{max}}",
"queueCapacityFull_one": "{{count}} of {{max}} · full",
@@ -35,22 +69,43 @@
"queueChipExpand_other": "{{count}} queued messages, click to expand",
"queueCount_one": "{{count}}",
"queueCount_other": "{{count}}",
"queueEditAlreadySent": "Message already sent — agent picked it up before your edit landed.",
"queueEditSaveFailed": "Failed to save edit. Please try again.",
"queueFullSuffix": "· full",
"queued": "Queued",
"queuedCount_one": "{{count}} queued",
"queuedCount_other": "{{count}} queued",
"queuedMessages": "Queued messages",
"queueEditAlreadySent": "Message already sent — agent picked it up before your edit landed.",
"queueEditSaveFailed": "Failed to save edit. Please try again.",
"queueFullSuffix": "· full",
"quickChatDialogIntro": "Start a quick conversation with an agent without creating a formal task.",
"quickChatIntro": "Chat with an agent about an idea, question, or codebase.",
"quickChatIntroSecondary": "Quick chats stay outside your task board.",
"removeQueuedMessage": "Remove queued message",
"renameChat": "Rename chat",
"renameSavedOnThisDeviceOnly": "Rename saved on this device only",
"renameSyncFailedDescription": "We could not sync the new name to your other devices.",
"repositories": "Repositories",
"repositoriesHelp": "Add repository context to focus on specific code and branches.",
"repositoryContextHelp": "Kandev uses an isolated worktree from the selected branch. Uncommitted local changes are not included.",
"repositoryOptional": "Repository (optional)",
"resizeQuickChatFromLeft": "Resize quick chat from left",
"resizeQuickChatFromRight": "Resize quick chat from right",
"retryAttachmentUpload": "Retry attachment upload",
"runNext": "Run next",
"runNextQueuedMessage": "Run next queued message",
"selectAgent": "Select agent",
"selectARepository": "Select a repository...",
"startChat": "Start Chat",
"startChatAction": "Start chat",
"starting": "Starting...",
"startingChat": "Starting chat...",
"startNewChat": "Start new chat",
"technicalDetails": "Technical details",
"providerQuotaTitle": "{{provider}} usage limit reached",
"providerQuotaModel": "Model: {{model}}",
"providerQuotaReset": "Capacity resets {{resetAt}}.",
"providerQuotaResetUnknown": "Try again when the provider makes capacity available.",
"providerQuotaProviderFallback": "Provider",
"theAssociatedWorktree": "The associated worktree",
"theTaskAndItsData": "The task and its data",
"thisActionCannotBeUndone": "This action cannot be undone.",
"turnGroupToolCalls_one": "{{count}} tool call",
"turnGroupToolCalls_other": "{{count}} tool calls"
"turnGroupToolCalls_other": "{{count}} tool calls",
"unknownError": "Unknown error",
"useWorkspaceDefault": "Use workspace default...",
"useWorkspaceDefaultOption": "Use workspace default"
}
+80
View File
@@ -0,0 +1,80 @@
{
"apply": "Apply",
"applyLatestAgentChangesToFile": "Apply latest agent changes to this file",
"boldCmdB": "Bold (Cmd+B)",
"comment": "Comment",
"commentAdded": "Comment added",
"commentCmdShiftC": "Comment (Cmd+Shift+C)",
"commentCount_one": "{{count}} comment",
"commentCount_other": "{{count}} comments",
"commentDeleted": "Comment deleted",
"commentMarkerTitle_one": "{{count}} comment - click to view",
"commentMarkerTitle_other": "{{count}} comments - click to view",
"commentSent": "Comment sent",
"commentUpdated": "Comment updated",
"commentWillBeSentWithNextMessage": "Your comment will be sent with your next message.",
"copyAllChangedLines": "Copy all changed lines",
"copyCode": "Copy code",
"copyDiff": "Copy diff",
"copyLine": "Copy line {{lineNumber}}",
"copyPath": "Copy path",
"default": "default",
"deleteFile": "Delete file",
"disableWordWrap": "Disable word wrap",
"enableWordWrap": "Enable word wrap",
"failedToSendComment": "Failed to send comment",
"foldUnchangedLines": "Fold unchanged lines",
"hideDiffIndicators": "Hide diff indicators",
"highlight": "Highlight",
"inlineCode": "Inline code",
"insertBlock": "Insert block",
"italicCmdI": "Italic (Cmd+I)",
"link": "Link",
"loadingDiff": "Loading diff...",
"loadingEditor": "Loading editor...",
"noDiffAvailable": "No diff available",
"noEditorsConfigured": "No editors configured",
"openFolder": "Open folder",
"openWith": "Open with...",
"pasteLink": "Paste link...",
"pleaseTryAgain": "Please try again.",
"previewMarkdown": "Preview markdown",
"queuedForTheAgent": "Queued for the agent.",
"reload": "Reload",
"revertAllChanges": "Revert all changes",
"revertChanges": "Revert changes",
"saving": "Saving...",
"sentToTheAgent": "Sent to the agent.",
"showAllLines": "Show all lines",
"showDiffIndicators": "Show diff indicators",
"slashBlockquote": "Blockquote",
"slashBlockquoteDescription": "Quote text",
"slashBulletList": "Bullet List",
"slashBulletListDescription": "Unordered list",
"slashCategoryBlocks": "Blocks",
"slashCategoryLists": "Lists",
"slashCategoryText": "Text",
"slashCodeBlock": "Code Block",
"slashCodeBlockDescription": "Fenced code",
"slashDivider": "Divider",
"slashDividerDescription": "Horizontal line",
"slashHeading1": "Heading 1",
"slashHeading1Description": "Large heading",
"slashHeading2": "Heading 2",
"slashHeading2Description": "Medium heading",
"slashHeading3": "Heading 3",
"slashHeading3Description": "Small heading",
"slashNumberedList": "Numbered List",
"slashNumberedListDescription": "Ordered list",
"slashTable": "Table",
"slashTableDescription": "3x3 table",
"slashTaskList": "Task List",
"slashTaskListDescription": "Checklist items",
"startTyping": "Start typing...",
"strikethroughCmdShiftX": "Strikethrough (Cmd+Shift+X)",
"switchToSplitView": "Switch to split view",
"switchToUnifiedView": "Switch to unified view",
"toggleCode": "Toggle code",
"toggleWordWrap": "Toggle word wrap",
"underlineCmdU": "Underline (Cmd+U)"
}
+80 -25
View File
@@ -1,32 +1,66 @@
{
"addToChatContext": "Àďď ţō ćĥàţ ćōńţēxţ",
"aboutRepositoryContext": "Àƀōũţ ŕēƥōśĩţōŕŷ ćōńţēxţ",
"addedToChatContext": "{{name}} àďďēď ţō ćĥàţ ćōńţēxţ",
"collapseMessage": "Ćōĺĺàƥśē ḿēśśàĝē",
"collapseQueuedMessages": "Ćōĺĺàƥśē qũēũēď ḿēśśàĝēś",
"clearAll": "Ćĺēàŕ àĺĺ",
"clearAllQueuedMessages": "Ćĺēàŕ àĺĺ qũēũēď ḿēśśàĝēś",
"addRepository": "Àďď ŕēƥōśĩţōŕŷ",
"addToChatContext": "Àďď ţō ćĥàţ ćōńţēxţ",
"agentOptional": "Àĝēńţ (ōƥţĩōńàĺ)",
"agentProfile": "Àĝēńţ ƥŕōƒĩĺē",
"agentProfileHelp": "Ćĥōōśē ţĥē àĝēńţ ƒōŕ ţĥĩś ćōńvēŕśàţĩōń.",
"allConversationHistory": "Àĺĺ ćōńvēŕśàţĩōń ĥĩśţōŕŷ",
"allWorkspaceRepositoriesAdded": "Àĺĺ ŵōŕķśƥàćē ŕēƥōśĩţōŕĩēś àŕē àĺŕēàďŷ àďďēď",
"attachmentDeliveryFile": "Ƒĩĺē",
"attachmentDeliveryMode": "Àţţàćĥḿēńţ ďēĺĩvēŕŷ ḿōďē",
"attachmentDeliveryPathDescription": "Ũƥĺōàď ĩńţō ţĥē ŵōŕķśƥàćē śō ţĥē àĝēńţ ćàń ŕēàď ōŕ ēďĩţ ţĥē ƒĩĺē.",
"attachmentDeliveryPrompt": "Ƥŕōḿƥţ",
"attachmentDeliveryPromptDescription": "Śēńď àś ƥŕōḿƥţ ćōńţēxţ ƒōŕ vĩśũàĺ ũńďēŕśţàńďĩńĝ. Ţĥē àĝēńţ ŵĩĺĺ ńōţ ĝēţ à ƒĩĺē ƥàţĥ.",
"attachmentFallbackName": "Àţţàćĥḿēńţ",
"attachmentMessageAlt": "Àţţàćĥḿēńţ {{index}}",
"attachmentPreviewAlt": "Ƥŕēvĩēŵ",
"attachmentPreviewFullAlt": "Ƒũĺĺ śĩźē ƥŕēvĩēŵ",
"attachmentUploadFailed": "Àţţàćĥḿēńţ ũƥĺōàď ƒàĩĺēď",
"attachmentUploadPending": "Ũƥĺōàďĩńĝ àţţàćĥḿēńţ…",
"attachmentUploadPendingSubmit": "Ƒĩńĩśĥ ũƥĺōàďĩńĝ àţţàćĥḿēńţś ƀēƒōŕē śēńďĩńĝ.",
"attachmentUploadFailed": "Àţţàćĥḿēńţ ũƥĺōàď ƒàĩĺēď",
"retryAttachmentUpload": "Ŕēţŕŷ àţţàćĥḿēńţ ũƥĺōàď",
"attachmentPreviewAlt": "Ƥŕēvĩēŵ",
"attachmentMessageAlt": "Àţţàćĥḿēńţ {{index}}",
"attachmentFallbackName": "Àţţàćĥḿēńţ",
"attachmentPreviewFullAlt": "Ƒũĺĺ śĩźē ƥŕēvĩēŵ",
"attachmentDeliveryMode": "Àţţàćĥḿēńţ ďēĺĩvēŕŷ ḿōďē",
"attachmentDeliveryPrompt": "Ƥŕōḿƥţ",
"attachmentDeliveryFile": "Ƒĩĺē",
"attachmentDeliveryPromptDescription": "Śēńď àś ƥŕōḿƥţ ćōńţēxţ ƒōŕ vĩśũàĺ ũńďēŕśţàńďĩńĝ. Ţĥē àĝēńţ ŵĩĺĺ ńōţ ĝēţ à ƒĩĺē ƥàţĥ.",
"attachmentDeliveryPathDescription": "Ũƥĺōàď ĩńţō ţĥē ŵōŕķśƥàćē śō ţĥē àĝēńţ ćàń ŕēàď ōŕ ēďĩţ ţĥē ƒĩĺē.",
"chatTabName": "Ćĥàţ {{index}}",
"clarificationNeeded": "Ćĺàŕĩƒĩćàţĩōń ńēēďēď",
"clearAll": "Ćĺēàŕ àĺĺ",
"clearAllQueuedMessages": "Ćĺēàŕ àĺĺ qũēũēď ḿēśśàĝēś",
"close": "Ćĺōśē {{name}}",
"closeQuickChat": "Ćĺōśē qũĩćķ ćĥàţ",
"collapseClarification": "Ćōĺĺàƥśē ćĺàŕĩƒĩćàţĩōń",
"collapseMessage": "Ćōĺĺàƥśē ḿēśśàĝē",
"collapseQueuedMessages": "Ćōĺĺàƥśē qũēũēď ḿēśśàĝēś",
"configChatPlaceholder": "Àśķ àńŷţĥĩńĝ àƀōũţ ŷōũŕ ćōńƒĩĝũŕàţĩōń...",
"configurationChat": "Ćōńƒĩĝũŕàţĩōń ćĥàţ",
"configurationChatDescription": "Ĺēţ ţĥē àĝēńţ ũƥďàţē Ķàńďēv śēţţĩńĝś, ŵōŕķƒĺōŵś, àĝēńţ ƥŕōƒĩĺēś, àńď ḾĆƤ ćōńƒĩĝũŕàţĩōń.",
"configurationChatTab": "Ćōńƒĩĝũŕàţĩōń Ćĥàţ",
"delete": "Ďēĺēţē",
"deleteQuickChat": "Ďēĺēţē Qũĩćķ Ćĥàţ?",
"deleteQuickChatIntro": "Ţĥĩś ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďēĺēţē ţĥĩś qũĩćķ ćĥàţ śēśśĩōń, ĩńćĺũďĩńĝ:",
"doubleClickToRename": "Ďōũƀĺē-ćĺĩćķ ţō ŕēńàḿē",
"editQueuedMessage": "Ēďĩţ qũēũēď ḿēśśàĝē",
"expandClarification": "Ēxƥàńď ćĺàŕĩƒĩćàţĩōń",
"expandMessage": "Ēxƥàńď ḿēśśàĝē",
"failedToClearQueuedMessages": "Ƒàĩĺēď ţō ćĺēàŕ qũēũēď ḿēśśàĝēś.",
"failedToDeleteQuickChat": "Ƒàĩĺēď ţō ďēĺēţē qũĩćķ ćĥàţ",
"failedToMergeQueuedMessages": "Ƒàĩĺēď ţō ḿēŕĝē qũēũēď ḿēśśàĝēś.",
"failedToRemoveQueuedMessage": "Ƒàĩĺēď ţō ŕēḿōvē qũēũēď ḿēśśàĝē.",
"failedToRunQueuedMessage": "Ƒàĩĺēď ţō ŕũń qũēũēď ḿēśśàĝē.",
"failedToStartQuickChat": "Ƒàĩĺēď ţō śţàŕţ qũĩćķ ćĥàţ",
"fileTreeActions": "Ƒĩĺē àćţĩōńś",
"loadingRepositories": "Ĺōàďĩńĝ ŕēƥōśĩţōŕĩēś",
"mergeReferenceOverflow": "Ţōō ḿàńŷ ēńţĩţŷ ŕēƒēŕēńćēś ţō ḿēŕĝē ţĥēśē qũēũēď ḿēśśàĝēś.",
"mergeWithAbove": "Ḿēŕĝē ŵĩţĥ àƀōvē",
"newChatTab": "Ńēŵ Ćĥàţ",
"newQuickChat": "Ńēŵ Qũĩćķ Ćĥàţ",
"noAgentsAvailable": "Ńō àĝēńţś àvàĩĺàƀĺē",
"noRepositoriesAvailableInWorkspace": "Ńō ŕēƥōśĩţōŕĩēś àvàĩĺàƀĺē ĩń ţĥĩś ŵōŕķśƥàćē",
"noRepository": "Ńō ŕēƥōśĩţōŕŷ",
"optional": "(ōƥţĩōńàĺ)",
"providerQuotaModel": "Ḿōďēĺ: {{model}}",
"providerQuotaProviderFallback": "Ƥŕōvĩďēŕ",
"providerQuotaReset": "Ćàƥàćĩţŷ ŕēśēţś {{resetAt}}.",
"providerQuotaResetUnknown": "Ţŕŷ àĝàĩń ŵĥēń ţĥē ƥŕōvĩďēŕ ḿàķēś ćàƥàćĩţŷ àvàĩĺàƀĺē.",
"providerQuotaTitle": "{{provider}} ũśàĝē ĺĩḿĩţ ŕēàćĥēď",
"queueCapacity_one": "{{count}} ōƒ {{max}}",
"queueCapacity_other": "{{count}} ōƒ {{max}}",
"queueCapacityFull_one": "{{count}} ōƒ {{max}} · ƒũĺĺ",
@@ -35,22 +69,43 @@
"queueChipExpand_other": "{{count}} qũēũēď ḿēśśàĝēś, ćĺĩćķ ţō ēxƥàńď",
"queueCount_one": "{{count}}",
"queueCount_other": "{{count}}",
"queueEditAlreadySent": "Ḿēśśàĝē àĺŕēàďŷ śēńţ — àĝēńţ ƥĩćķēď ĩţ ũƥ ƀēƒōŕē ŷōũŕ ēďĩţ ĺàńďēď.",
"queueEditSaveFailed": "Ƒàĩĺēď ţō śàvē ēďĩţ. Ƥĺēàśē ţŕŷ àĝàĩń.",
"queueFullSuffix": "· ƒũĺĺ",
"queued": "Qũēũēď",
"queuedCount_one": "{{count}} qũēũēď",
"queuedCount_other": "{{count}} qũēũēď",
"queuedMessages": "Qũēũēď ḿēśśàĝēś",
"queueEditAlreadySent": "Ḿēśśàĝē àĺŕēàďŷ śēńţ — àĝēńţ ƥĩćķēď ĩţ ũƥ ƀēƒōŕē ŷōũŕ ēďĩţ ĺàńďēď.",
"queueEditSaveFailed": "Ƒàĩĺēď ţō śàvē ēďĩţ. Ƥĺēàśē ţŕŷ àĝàĩń.",
"queueFullSuffix": "· ƒũĺĺ",
"quickChatDialogIntro": "Śţàŕţ à qũĩćķ ćōńvēŕśàţĩōń ŵĩţĥ àń àĝēńţ ŵĩţĥōũţ ćŕēàţĩńĝ à ƒōŕḿàĺ ţàśķ.",
"quickChatIntro": "Ćĥàţ ŵĩţĥ àń àĝēńţ àƀōũţ àń ĩďēà, qũēśţĩōń, ōŕ ćōďēƀàśē.",
"quickChatIntroSecondary": "Qũĩćķ ćĥàţś śţàŷ ōũţśĩďē ŷōũŕ ţàśķ ƀōàŕď.",
"removeQueuedMessage": "Ŕēḿōvē qũēũēď ḿēśśàĝē",
"renameChat": "Ŕēńàḿē ćĥàţ",
"renameSavedOnThisDeviceOnly": "Ŕēńàḿē śàvēď ōń ţĥĩś ďēvĩćē ōńĺŷ",
"renameSyncFailedDescription": "Ŵē ćōũĺď ńōţ śŷńć ţĥē ńēŵ ńàḿē ţō ŷōũŕ ōţĥēŕ ďēvĩćēś.",
"repositories": "Ŕēƥōśĩţōŕĩēś",
"repositoriesHelp": "Àďď ŕēƥōśĩţōŕŷ ćōńţēxţ ţō ƒōćũś ōń śƥēćĩƒĩć ćōďē àńď ƀŕàńćĥēś.",
"repositoryContextHelp": "Ķàńďēv ũśēś àń ĩśōĺàţēď ŵōŕķţŕēē ƒŕōḿ ţĥē śēĺēćţēď ƀŕàńćĥ. Ũńćōḿḿĩţţēď ĺōćàĺ ćĥàńĝēś àŕē ńōţ ĩńćĺũďēď.",
"repositoryOptional": "Ŕēƥōśĩţōŕŷ (ōƥţĩōńàĺ)",
"resizeQuickChatFromLeft": "Ŕēśĩźē qũĩćķ ćĥàţ ƒŕōḿ ĺēƒţ",
"resizeQuickChatFromRight": "Ŕēśĩźē qũĩćķ ćĥàţ ƒŕōḿ ŕĩĝĥţ",
"retryAttachmentUpload": "Ŕēţŕŷ àţţàćĥḿēńţ ũƥĺōàď",
"runNext": "Ŕũń ńēxţ",
"runNextQueuedMessage": "Ŕũń ńēxţ qũēũēď ḿēśśàĝē",
"selectAgent": "Śēĺēćţ àĝēńţ",
"selectARepository": "Śēĺēćţ à ŕēƥōśĩţōŕŷ...",
"startChat": "Śţàŕţ Ćĥàţ",
"startChatAction": "Śţàŕţ ćĥàţ",
"starting": "Śţàŕţĩńĝ...",
"startingChat": "Śţàŕţĩńĝ ćĥàţ...",
"startNewChat": "Śţàŕţ ńēŵ ćĥàţ",
"technicalDetails": "Ţēćĥńĩćàĺ ďēţàĩĺś",
"providerQuotaTitle": "{{provider}} ũśàĝē ĺĩḿĩţ ŕēàćĥēď",
"providerQuotaModel": "Ḿōďēĺ: {{model}}",
"providerQuotaReset": "Ćàƥàćĩţŷ ŕēśēţś {{resetAt}}.",
"providerQuotaResetUnknown": "Ţŕŷ àĝàĩń ŵĥēń ţĥē ƥŕōvĩďēŕ ḿàķēś ćàƥàćĩţŷ àvàĩĺàƀĺē.",
"providerQuotaProviderFallback": "Ƥŕōvĩďēŕ",
"theAssociatedWorktree": "Ţĥē àśśōćĩàţēď ŵōŕķţŕēē",
"theTaskAndItsData": "Ţĥē ţàśķ àńď ĩţś ďàţà",
"thisActionCannotBeUndone": "Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
"turnGroupToolCalls_one": "{{count}} ţōōĺ ćàĺĺ",
"turnGroupToolCalls_other": "{{count}} ţōōĺ ćàĺĺś"
"turnGroupToolCalls_other": "{{count}} ţōōĺ ćàĺĺś",
"unknownError": "Ũńķńōŵń ēŕŕōŕ",
"useWorkspaceDefault": "Ũśē ŵōŕķśƥàćē ďēƒàũĺţ...",
"useWorkspaceDefaultOption": "Ũśē ŵōŕķśƥàćē ďēƒàũĺţ"
}
+80
View File
@@ -0,0 +1,80 @@
{
"apply": "Àƥƥĺŷ",
"applyLatestAgentChangesToFile": "Àƥƥĺŷ ĺàţēśţ àĝēńţ ćĥàńĝēś ţō ţĥĩś ƒĩĺē",
"boldCmdB": "Ɓōĺď (Ćḿď+Ɓ)",
"comment": "Ćōḿḿēńţ",
"commentAdded": "Ćōḿḿēńţ àďďēď",
"commentCmdShiftC": "Ćōḿḿēńţ (Ćḿď+Śĥĩƒţ+Ć)",
"commentCount_one": "{{count}} ćōḿḿēńţ",
"commentCount_other": "{{count}} ćōḿḿēńţś",
"commentDeleted": "Ćōḿḿēńţ ďēĺēţēď",
"commentMarkerTitle_one": "{{count}} ćōḿḿēńţ - ćĺĩćķ ţō vĩēŵ",
"commentMarkerTitle_other": "{{count}} ćōḿḿēńţś - ćĺĩćķ ţō vĩēŵ",
"commentSent": "Ćōḿḿēńţ śēńţ",
"commentUpdated": "Ćōḿḿēńţ ũƥďàţēď",
"commentWillBeSentWithNextMessage": "Ŷōũŕ ćōḿḿēńţ ŵĩĺĺ ƀē śēńţ ŵĩţĥ ŷōũŕ ńēxţ ḿēśśàĝē.",
"copyAllChangedLines": "Ćōƥŷ àĺĺ ćĥàńĝēď ĺĩńēś",
"copyCode": "Ćōƥŷ ćōďē",
"copyDiff": "Ćōƥŷ ďĩƒƒ",
"copyLine": "Ćōƥŷ ĺĩńē {{lineNumber}}",
"copyPath": "Ćōƥŷ ƥàţĥ",
"default": "ďēƒàũĺţ",
"deleteFile": "Ďēĺēţē ƒĩĺē",
"disableWordWrap": "Ďĩśàƀĺē ŵōŕď ŵŕàƥ",
"enableWordWrap": "Ēńàƀĺē ŵōŕď ŵŕàƥ",
"failedToSendComment": "Ƒàĩĺēď ţō śēńď ćōḿḿēńţ",
"foldUnchangedLines": "Ƒōĺď ũńćĥàńĝēď ĺĩńēś",
"hideDiffIndicators": "Ĥĩďē ďĩƒƒ ĩńďĩćàţōŕś",
"highlight": "Ĥĩĝĥĺĩĝĥţ",
"inlineCode": "Ĩńĺĩńē ćōďē",
"insertBlock": "Ĩńśēŕţ ƀĺōćķ",
"italicCmdI": "Ĩţàĺĩć (Ćḿď+Ĩ)",
"link": "Ĺĩńķ",
"loadingDiff": "Ĺōàďĩńĝ ďĩƒƒ...",
"loadingEditor": "Ĺōàďĩńĝ ēďĩţōŕ...",
"noDiffAvailable": "Ńō ďĩƒƒ àvàĩĺàƀĺē",
"noEditorsConfigured": "Ńō ēďĩţōŕś ćōńƒĩĝũŕēď",
"openFolder": "Ōƥēń ƒōĺďēŕ",
"openWith": "Ōƥēń ŵĩţĥ...",
"pasteLink": "Ƥàśţē ĺĩńķ...",
"pleaseTryAgain": "Ƥĺēàśē ţŕŷ àĝàĩń.",
"previewMarkdown": "Ƥŕēvĩēŵ ḿàŕķďōŵń",
"queuedForTheAgent": "Qũēũēď ƒōŕ ţĥē àĝēńţ.",
"reload": "Ŕēĺōàď",
"revertAllChanges": "Ŕēvēŕţ àĺĺ ćĥàńĝēś",
"revertChanges": "Ŕēvēŕţ ćĥàńĝēś",
"saving": "Śàvĩńĝ...",
"sentToTheAgent": "Śēńţ ţō ţĥē àĝēńţ.",
"showAllLines": "Śĥōŵ àĺĺ ĺĩńēś",
"showDiffIndicators": "Śĥōŵ ďĩƒƒ ĩńďĩćàţōŕś",
"slashBlockquote": "Ɓĺōćķqũōţē",
"slashBlockquoteDescription": "Qũōţē ţēxţ",
"slashBulletList": "Ɓũĺĺēţ Ĺĩśţ",
"slashBulletListDescription": "Ũńōŕďēŕēď ĺĩśţ",
"slashCategoryBlocks": "Ɓĺōćķś",
"slashCategoryLists": "Ĺĩśţś",
"slashCategoryText": "Ţēxţ",
"slashCodeBlock": "Ćōďē Ɓĺōćķ",
"slashCodeBlockDescription": "Ƒēńćēď ćōďē",
"slashDivider": "Ďĩvĩďēŕ",
"slashDividerDescription": "Ĥōŕĩźōńţàĺ ĺĩńē",
"slashHeading1": "Ĥēàďĩńĝ 1",
"slashHeading1Description": "Ĺàŕĝē ĥēàďĩńĝ",
"slashHeading2": "Ĥēàďĩńĝ 2",
"slashHeading2Description": "Ḿēďĩũḿ ĥēàďĩńĝ",
"slashHeading3": "Ĥēàďĩńĝ 3",
"slashHeading3Description": "Śḿàĺĺ ĥēàďĩńĝ",
"slashNumberedList": "Ńũḿƀēŕēď Ĺĩśţ",
"slashNumberedListDescription": "Ōŕďēŕēď ĺĩśţ",
"slashTable": "Ţàƀĺē",
"slashTableDescription": "3x3 ţàƀĺē",
"slashTaskList": "Ţàśķ Ĺĩśţ",
"slashTaskListDescription": "Ćĥēćķĺĩśţ ĩţēḿś",
"startTyping": "Śţàŕţ ţŷƥĩńĝ...",
"strikethroughCmdShiftX": "Śţŕĩķēţĥŕōũĝĥ (Ćḿď+Śĥĩƒţ+X)",
"switchToSplitView": "Śŵĩţćĥ ţō śƥĺĩţ vĩēŵ",
"switchToUnifiedView": "Śŵĩţćĥ ţō ũńĩƒĩēď vĩēŵ",
"toggleCode": "Ţōĝĝĺē ćōďē",
"toggleWordWrap": "Ţōĝĝĺē ŵōŕď ŵŕàƥ",
"underlineCmdU": "Ũńďēŕĺĩńē (Ćḿď+Ũ)"
}