feat(i18n): localize editors and quick-chat (#2300)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}",
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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": "Ũśē ŵōŕķśƥàćē ďēƒàũĺţ"
|
||||
}
|
||||
|
||||
@@ -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": "Ũńďēŕĺĩńē (Ćḿď+Ũ)"
|
||||
}
|
||||
Reference in New Issue
Block a user