feat(i18n): localize loose components directly under components/ (#2317)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-07 04:57:02 +02:00
committed by GitHub
parent dc89d494fe
commit 7005b85481
27 changed files with 1132 additions and 354 deletions
+18 -9
View File
@@ -3,6 +3,8 @@
import { useState } from "react";
import { IconRefresh } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type BranchRefreshButtonProps = {
onRefresh: () => void;
@@ -23,18 +25,19 @@ export function BranchRefreshButton({
testId = "branch-refresh-button",
touchTarget = false,
}: BranchRefreshButtonProps) {
const { t } = useTranslation();
// Controlled open so the tooltip only reacts to hover, not focus.
// Radix Popover auto-focuses the first focusable child when it opens, which
// would otherwise trigger this tooltip the moment the dropdown is opened.
const [open, setOpen] = useState(false);
const hasError = Boolean(fetchError);
const tooltip = formatRefreshTooltip(label, fetchedAt, refreshing, fetchError);
const tooltip = formatRefreshTooltip(t, label, fetchedAt, refreshing, fetchError);
return (
<Tooltip open={open} onOpenChange={setOpen}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={`Refresh ${label}`}
aria-label={t("common:refreshAriaLabel", { label })}
data-testid={testId}
onClick={(e) => {
e.preventDefault();
@@ -58,20 +61,26 @@ export function BranchRefreshButton({
);
}
// `label` stays untranslated: it is compared with `===` below, and callers pass
// it as a free-form noun ("branches", "repositories"). It travels through
// interpolation instead.
function formatRefreshTooltip(
t: TFunction,
label: string,
fetchedAt: string | undefined,
refreshing: boolean | undefined,
fetchError: string | undefined,
) {
if (refreshing) return `Refreshing ${label}...`;
if (fetchError) return `Last refresh failed: ${fetchError}`;
if (!fetchedAt) return initialRefreshTooltip(label);
if (refreshing) return t("common:refreshingLabel", { label });
if (fetchError) return t("common:lastRefreshFailed", { error: fetchError });
if (!fetchedAt) return initialRefreshTooltip(t, label);
const date = new Date(fetchedAt);
if (Number.isNaN(date.getTime())) return initialRefreshTooltip(label);
return `Refresh ${label} (last fetched ${date.toLocaleTimeString()})`;
if (Number.isNaN(date.getTime())) return initialRefreshTooltip(t, label);
return t("common:refreshLabelLastFetched", { label, time: date.toLocaleTimeString() });
}
function initialRefreshTooltip(label: string) {
return label === "branches" ? "Refresh branches (git fetch)" : `Refresh ${label}`;
function initialRefreshTooltip(t: TFunction, label: string) {
return label === "branches"
? t("common:refreshBranchesGitFetch")
: t("common:refreshAriaLabel", { label });
}
+3 -1
View File
@@ -1,14 +1,16 @@
import { IconTerminal2 } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
// Marks an agent profile that runs in CLI passthrough mode — the prompt is
// auto-injected into a raw terminal rather than sent as a structured request.
// Shown next to passthrough profiles in the manual create dialog and the
// Linear/Jira/GitHub watcher dialogs so the two paths stay visually consistent.
export function CliModeIcon({ className }: { className?: string }) {
const { t } = useTranslation();
return (
<IconTerminal2
className={className ?? "size-3.5 text-muted-foreground"}
title="CLI mode — your prompt will be auto-injected into the terminal"
title={t("common:cliModeYourPromptWillBe")}
/>
);
}
+51 -30
View File
@@ -2,6 +2,7 @@
import { useEffect, type Dispatch, type SetStateAction } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { formatDistanceToNow } from "date-fns";
import { IconArchive, IconArrowRight, IconHammer, IconLoader2 } from "@tabler/icons-react";
import {
@@ -225,19 +226,25 @@ function CommandsListContent({
repoMap,
onTaskSelect,
}: CommandsListContentProps) {
const { t } = useTranslation();
const hasInlineResults = taskResults.length > 0 || isSearching;
return (
<>
{!hasInlineResults && !isSearching && <CommandEmpty>No commands found.</CommandEmpty>}
{!hasInlineResults && !isSearching && (
<CommandEmpty>{t("common:noCommandsFound")}</CommandEmpty>
)}
{isSearching && taskResults.length === 0 && (
<CommandGroup heading="Active Tasks" forceMount>
<CommandGroup heading={t("common:activeTasks")} forceMount>
<div className="flex items-center justify-center py-3">
<IconLoader2 className="size-3.5 animate-spin text-muted-foreground" />
</div>
</CommandGroup>
)}
{taskResults.length > 0 && (
<CommandGroup heading={search.trim() ? "Tasks" : "Active Tasks"} forceMount>
<CommandGroup
heading={search.trim() ? t("common:tasks") : t("common:activeTasks")}
forceMount
>
{taskResults.map((task) => (
<TaskResultItem
key={task.id}
@@ -283,6 +290,7 @@ function FileSearchContent({
sessionId,
onSelect,
}: FileSearchContentProps) {
const { t } = useTranslation();
const getRepoDisplayName = useRepoDisplayName(sessionId);
if (isSearching && files.length === 0) {
return (
@@ -291,8 +299,9 @@ function FileSearchContent({
</div>
);
}
if (search.trim() && files.length === 0) return <CommandEmpty>No files found.</CommandEmpty>;
if (!search.trim()) return <CommandEmpty>Type to search files...</CommandEmpty>;
if (search.trim() && files.length === 0)
return <CommandEmpty>{t("common:noFilesFound")}</CommandEmpty>;
if (!search.trim()) return <CommandEmpty>{t("common:typeToSearchFiles")}</CommandEmpty>;
const groups = groupByRepositoryName(files, (file) => file.repository_name);
const singleRepo = isSingleRepoGroup(groups);
@@ -301,8 +310,10 @@ function FileSearchContent({
key={group.repositoryName || "workspace"}
heading={
singleRepo
? "Files"
: (getRepoDisplayName(group.repositoryName) ?? group.repositoryName ?? "Workspace")
? t("common:files")
: (getRepoDisplayName(group.repositoryName) ??
group.repositoryName ??
t("common:workspace"))
}
forceMount
data-testid="file-search-repo-group"
@@ -333,27 +344,33 @@ function FileSearchContent({
));
}
function getInputPlaceholder(mode: CommandPanelMode, inputCommand: CommandItemType | null) {
if (mode === "input") return inputCommand?.inputPlaceholder ?? "Enter value...";
if (mode === "search-tasks") return "Search for tasks...";
if (mode === MODE_SEARCH_FILES) return "Search for files...";
if (mode === MODE_SEARCH_CONTENT) return "Search task contents…";
return "Type a command...";
// `mode` stays an untranslated discriminant — it is compared with `===`
// throughout. Only the labels it selects are copy.
function getInputPlaceholder(
t: TFunction,
mode: CommandPanelMode,
inputCommand: CommandItemType | null,
) {
if (mode === "input") return inputCommand?.inputPlaceholder ?? t("common:enterValue");
if (mode === "search-tasks") return t("common:searchForTasks");
if (mode === MODE_SEARCH_FILES) return t("common:searchForFiles");
if (mode === MODE_SEARCH_CONTENT) return t("common:searchTaskContents");
return t("common:typeACommand");
}
function getEnterLabel(mode: CommandPanelMode) {
if (mode === "input") return "Confirm";
function getEnterLabel(t: TFunction, mode: CommandPanelMode) {
if (mode === "input") return t("common:confirm");
if (mode === "search-tasks" || mode === MODE_SEARCH_FILES || mode === MODE_SEARCH_CONTENT) {
return "Open";
return t("common:open");
}
return "Select";
return t("common:select");
}
function getModeLabel(mode: CommandPanelMode, inputCommand: CommandItemType | null) {
function getModeLabel(t: TFunction, mode: CommandPanelMode, inputCommand: CommandItemType | null) {
if (mode === "input") return inputCommand?.label;
if (mode === "search-tasks") return "Tasks";
if (mode === MODE_SEARCH_FILES) return "Files";
if (mode === MODE_SEARCH_CONTENT) return "Contents";
if (mode === "search-tasks") return t("common:tasks");
if (mode === MODE_SEARCH_FILES) return t("common:files");
if (mode === MODE_SEARCH_CONTENT) return t("common:contents");
return null;
}
@@ -364,6 +381,7 @@ function CommandPanelFooter({
mode: CommandPanelMode;
workspaceSearchAvailable: boolean;
}) {
const { t } = useTranslation();
const isScopeMode = isCommandPanelScopeMode(mode);
const canSwitchScope = workspaceSearchAvailable && isScopeMode;
return (
@@ -373,29 +391,30 @@ function CommandPanelFooter({
<KbdGroup>
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
<span>Navigate</span>
<span>{t("common:navigate")}</span>
</KbdGroup>
{canSwitchScope && (
<KbdGroup>
<Kbd>Tab</Kbd>
<span>Switch mode</span>
<span>{t("common:switchMode")}</span>
</KbdGroup>
)}
</>
)}
<KbdGroup>
<Kbd>↵</Kbd>
<span>{getEnterLabel(mode)}</span>
<span>{getEnterLabel(t, mode)}</span>
</KbdGroup>
{!isScopeMode && (
<KbdGroup>
<Kbd>⌫</Kbd>
<span>Back</span>
<span>{t("common:back")}</span>
</KbdGroup>
)}
<KbdGroup>
{/* A key name, not copy — it labels the physical key. */}
<Kbd>esc</Kbd>
<span>Close</span>
<span>{t("common:close")}</span>
</KbdGroup>
</div>
);
@@ -442,9 +461,10 @@ function CommandPanelInputHeader({
goBack,
workspaceSearchAvailable,
}: CommandPanelViewProps) {
const { t } = useTranslation();
const isTopLevelMode = isCommandPanelScopeMode(mode);
const canSwitchScope = workspaceSearchAvailable && isTopLevelMode;
const modeLabel = getModeLabel(mode, inputCommand);
const modeLabel = getModeLabel(t, mode, inputCommand);
const onInputKeyDown = (event: React.KeyboardEvent) => {
if (
canSwitchScope &&
@@ -473,7 +493,7 @@ function CommandPanelInputHeader({
</button>
)}
<CommandInput
placeholder={getInputPlaceholder(mode, inputCommand)}
placeholder={getInputPlaceholder(t, mode, inputCommand)}
value={search}
onValueChange={setSearch}
onKeyDown={onInputKeyDown}
@@ -484,6 +504,7 @@ function CommandPanelInputHeader({
}
function CommandPanelResultList(props: CommandPanelViewProps) {
const { t } = useTranslation();
const {
mode,
inputCommand,
@@ -541,9 +562,9 @@ function CommandPanelResultList(props: CommandPanelViewProps) {
)}
{mode === "input" &&
(!search.trim() ? (
<CommandEmpty>{inputCommand?.inputPlaceholder ?? "Enter a value..."}</CommandEmpty>
<CommandEmpty>{inputCommand?.inputPlaceholder ?? t("common:enterAValue")}</CommandEmpty>
) : (
<CommandEmpty>Press Enter to confirm</CommandEmpty>
<CommandEmpty>{t("common:pressEnterToConfirm")}</CommandEmpty>
))}
</CommandList>
);
@@ -6,21 +6,27 @@ import type { ConfigurableShortcutId } from "@/lib/keyboard/shortcut-overrides";
import { getShortcut } from "@/lib/keyboard/shortcut-overrides";
import { formatShortcut } from "@/lib/keyboard/utils";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
export type CommandPanelScopeMode = "commands" | "search-files" | "search-content";
type ScopeOption = {
mode: CommandPanelScopeMode;
label: string;
/**
* Catalog key, resolved at render. `mode` stays the untranslated
* discriminant — it is compared with `===` in `isCommandPanelScopeMode` and
* threaded through the command-panel state.
*/
labelKey: string;
shortcutId: ConfigurableShortcutId;
};
const SCOPE_OPTIONS: ScopeOption[] = [
{ mode: "commands", label: "Commands", shortcutId: "SEARCH" },
{ mode: "search-files", label: "Files", shortcutId: "FILE_SEARCH" },
{ mode: "commands", labelKey: "common:scopeCommands", shortcutId: "SEARCH" },
{ mode: "search-files", labelKey: "common:scopeFiles", shortcutId: "FILE_SEARCH" },
{
mode: "search-content",
label: "Contents",
labelKey: "common:scopeContents",
shortcutId: "CONTENT_SEARCH",
},
];
@@ -46,26 +52,28 @@ export function CommandPanelScopeSwitcher({
mode: CommandPanelScopeMode;
onScopeChange: (mode: CommandPanelScopeMode) => void;
}) {
const { t } = useTranslation();
const keyboardShortcuts = useAppStore((state) => state.userSettings.keyboardShortcuts);
return (
<div
role="tablist"
aria-label="Command palette mode"
aria-label={t("common:commandPaletteMode")}
className="mr-1 flex h-10 shrink-0 items-stretch gap-0.5"
>
{SCOPE_OPTIONS.map((scope) => {
const active = mode === scope.mode;
const shortcut = formatShortcut(getShortcut(scope.shortcutId, keyboardShortcuts));
const label = t(scope.labelKey);
return (
<button
key={scope.mode}
type="button"
role="tab"
aria-label={scope.label}
aria-label={label}
aria-selected={active}
tabIndex={-1}
title={`${scope.label} (${shortcut})`}
title={t("common:scopeTitleWithShortcut", { label, shortcut })}
onMouseDown={(event) => event.preventDefault()}
onClick={() => onScopeChange(scope.mode)}
className={cn(
@@ -75,7 +83,7 @@ export function CommandPanelScopeSwitcher({
: "after:scale-x-75 after:opacity-0",
)}
>
<span>{scope.label}</span>
<span>{label}</span>
</button>
);
})}
@@ -30,7 +30,12 @@ import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { initializeLocalRepository } from "@/lib/api/domains/workspace-api";
import { createDirectory } from "@/lib/api/domains/fs-api";
import type { Repository } from "@/lib/types/http";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
// The returned strings are never rendered: the only caller uses this as a
// boolean gate (`!nameError`), so they stay English rather than becoming dead
// catalog entries.
export function validateLocalRepositoryName(name: string): string | null {
const trimmed = name.trim();
if (!trimmed) return "Enter a repository name.";
@@ -57,20 +62,27 @@ export type CreateLocalRepositorySurfaceProps = {
onCreated: (repository: Repository) => void;
};
// `context` and `requiresSwitch` stay logic; only the notices they select are
// copy. The profile name is user data, interpolated rather than translated.
function executorNotice(
t: TFunction,
selection: DirectLocalExecutorSelection | null,
context: NonNullable<CreateLocalRepositorySurfaceProps["context"]>,
): string {
if (context === "workspace") {
return "Creates an empty Git repository and registers it in this workspace.";
return t("common:createsAnEmptyGitRepository");
}
if (!selection) {
return "A direct local executor profile is required to create and use an empty repository.";
return t("common:aDirectLocalExecutorProfileIsRequired");
}
if (selection.requiresSwitch) {
return `Empty repositories run directly on this machine. This task will switch to “${selection.executorProfileName}”.`;
return t("common:emptyRepositoriesRunDirectlySwitch", {
profile: selection.executorProfileName,
});
}
return `This empty repository will run with “${selection.executorProfileName}” on this machine.`;
return t("common:thisEmptyRepositoryWillRunWith", {
profile: selection.executorProfileName,
});
}
type RepositoryLocationFieldsProps = {
@@ -90,10 +102,11 @@ function RepositoryLocationFields({
onParentPathChange,
onLoadTypedDirectory,
}: RepositoryLocationFieldsProps) {
const { t } = useTranslation();
return (
<>
<label className="block space-y-1.5 text-xs font-medium">
<span>Repository name</span>
<span>{t("common:repositoryName")}</span>
<Input
value={name}
onChange={(event) => onNameChange(event.target.value)}
@@ -104,7 +117,7 @@ function RepositoryLocationFields({
</label>
<div className="space-y-1.5">
<label htmlFor="local-repository-parent" className="text-xs font-medium">
Parent directory
{t("common:parentDirectory")}
</label>
<div className="flex min-w-0 items-center gap-2">
<Input
@@ -126,8 +139,8 @@ function RepositoryLocationFields({
className="size-11 sm:size-8"
onClick={onLoadTypedDirectory}
disabled={!parentPath}
aria-label="Browse parent directory"
title="Browse parent directory"
aria-label={t("common:browseParentDirectory")}
title={t("common:browseParentDirectory")}
>
<IconFolderOpen />
</Button>
@@ -135,9 +148,9 @@ function RepositoryLocationFields({
</div>
<div className="flex min-w-0 items-center gap-2 border-t border-border/70 pt-2 sm:col-span-2">
<IconFolder className="size-4 shrink-0 text-muted-foreground" />
<span className="shrink-0 text-xs text-muted-foreground">Destination</span>
<span className="shrink-0 text-xs text-muted-foreground">{t("common:destination")}</span>
<span className="truncate font-mono text-xs" title={targetPath || parentPath}>
{targetPath || parentPath || "Loading folder…"}
{targetPath || parentPath || t("common:loadingFolder")}
</span>
</div>
</>
@@ -156,6 +169,7 @@ function RepositoryFormDetails({
submitError,
...locationFields
}: RepositoryFormDetailsProps) {
const { t } = useTranslation();
return (
<div className="shrink-0 space-y-3 px-4 py-4">
<div className="grid gap-3 sm:grid-cols-[minmax(0,0.8fr)_minmax(0,1.6fr)]">
@@ -169,7 +183,7 @@ function RepositoryFormDetails({
}
>
<IconInfoCircle className="mt-0.5 size-3.5 shrink-0" />
<p>{executorNotice(executorSelection, context)}</p>
<p>{executorNotice(t, executorSelection, context)}</p>
</div>
{submitError ? (
<div
@@ -191,6 +205,7 @@ function CreateRepositoryFooter({
canSubmit: boolean;
submitting: boolean;
}) {
const { t } = useTranslation();
return (
<div className="flex shrink-0 justify-end border-t border-border px-4 pt-3 pb-[max(1rem,env(safe-area-inset-bottom))]">
<Button
@@ -199,7 +214,7 @@ function CreateRepositoryFooter({
disabled={!canSubmit}
>
<IconFolderPlus className="h-4 w-4" />
{submitting ? "Creating…" : "Create repository"}
{submitting ? t("common:creating") : t("common:createRepository")}
</Button>
</div>
);
@@ -213,6 +228,7 @@ function CreateRepositoryForm({
onCreated,
onDismiss,
}: CreateLocalRepositorySurfaceProps & { onDismiss: () => void }) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [parentPath, setParentPath] = useState("");
const [editingParentPath, setEditingParentPath] = useState(false);
@@ -250,7 +266,7 @@ function CreateRepositoryForm({
setName("");
onDismiss();
} catch (err) {
setSubmitError(err instanceof Error ? err.message : "Failed to create repository");
setSubmitError(err instanceof Error ? err.message : t("common:failedToCreateRepository"));
} finally {
setSubmitting(false);
}
@@ -313,6 +329,7 @@ function CreateRepositoryForm({
}
export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurfaceProps) {
const { t } = useTranslation();
const { isMobile } = useResponsiveBreakpoint();
const handleOpenChange = (open: boolean) => props.onOpenChange(open);
const form = <CreateRepositoryForm {...props} onDismiss={() => handleOpenChange(false)} />;
@@ -325,10 +342,8 @@ export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurface
className="h-[88dvh] max-h-[88dvh] min-w-0 overflow-hidden pb-0"
>
<DrawerHeader className="shrink-0 border-b border-border text-left">
<DrawerTitle>Create new repository</DrawerTitle>
<DrawerDescription>
Choose a folder or enter a new path on this machine.
</DrawerDescription>
<DrawerTitle>{t("common:createNewRepository")}</DrawerTitle>
<DrawerDescription>{t("common:chooseAFolderOrEnterA")}</DrawerDescription>
</DrawerHeader>
{form}
</DrawerContent>
@@ -343,10 +358,8 @@ export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurface
className="flex h-[min(640px,85dvh)] max-w-xl min-w-0 flex-col overflow-hidden p-0"
>
<DialogHeader className="shrink-0 border-b border-border px-4 py-3 pr-12">
<DialogTitle>Create new repository</DialogTitle>
<DialogDescription>
Choose a folder or enter a new path on this machine.
</DialogDescription>
<DialogTitle>{t("common:createNewRepository")}</DialogTitle>
<DialogDescription>{t("common:chooseAFolderOrEnterA")}</DialogDescription>
</DialogHeader>
{form}
</DialogContent>
@@ -10,6 +10,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@kandev/ui/alert-dialog";
import { useTranslation } from "react-i18next";
const MAX_VISIBLE_FILES = 20;
@@ -30,9 +31,15 @@ export function DiscardLocalChangesDialog({
onConfirm,
onCancel,
}: DiscardLocalChangesDialogProps) {
const { t } = useTranslation();
const visible = dirtyFiles.slice(0, MAX_VISIBLE_FILES);
const overflow = dirtyFiles.length - visible.length;
const target = repoPath ? ` in your local clone at ${repoPath}` : " in your local clone";
// One complete sentence per branch rather than a stem plus a " in your local
// clone at X" tail: the tail's position in the sentence is the translator's
// call, and a concatenated fragment freezes it here.
const description = repoPath
? t("common:discardLocalChangesAtPathDescription", { repoPath })
: t("common:discardLocalChangesDescription");
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -44,11 +51,8 @@ export function DiscardLocalChangesDialog({
onClick={(e) => e.stopPropagation()}
>
<AlertDialogHeader>
<AlertDialogTitle>Discard local changes?</AlertDialogTitle>
<AlertDialogDescription>
Starting this task will permanently discard the uncommitted changes
{target}. Back up anything you want to keep before continuing.
</AlertDialogDescription>
<AlertDialogTitle>{t("common:discardLocalChanges")}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<ul
className="max-h-48 overflow-auto rounded-md border border-border bg-muted/40 p-2 text-xs font-mono text-muted-foreground space-y-0.5"
@@ -64,7 +68,7 @@ export function DiscardLocalChangesDialog({
className="pt-1 text-[11px] italic text-muted-foreground/80"
data-testid="discard-local-changes-overflow"
>
+{overflow} more
{t("common:andMoreFiles", { count: overflow })}
</li>
)}
</ul>
@@ -74,14 +78,14 @@ export function DiscardLocalChangesDialog({
data-testid="discard-local-changes-cancel"
onClick={onCancel}
>
Cancel
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="discard-local-changes-confirm"
onClick={onConfirm}
>
Discard and continue
{t("common:discardAndContinue")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -5,6 +5,7 @@ import { Button } from "@kandev/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { GridSpinner } from "@/components/grid-spinner";
import { useTooltipMountGate } from "@/hooks/use-tooltip-mount-gate";
import { useTranslation } from "react-i18next";
type EnhancePromptButtonProps = {
onClick: () => void;
@@ -17,11 +18,12 @@ export function EnhancePromptButton({
isLoading,
isConfigured = true,
}: EnhancePromptButtonProps) {
const { t } = useTranslation();
const { tooltipOpenState, handleTooltipOpenChange } = useTooltipMountGate();
const isDisabled = !isConfigured || isLoading;
const tooltipText = isConfigured
? "Enhance prompt with AI"
: "Configure a utility agent in settings to enable AI enhancement";
? t("common:enhancePromptWithAi")
: t("common:configureAUtilityAgentToEnhance");
return (
<Tooltip open={tooltipOpenState} onOpenChange={handleTooltipOpenChange}>
@@ -40,7 +42,7 @@ export function EnhancePromptButton({
className="h-7 w-7 cursor-pointer hover:bg-muted/40 text-slate-400"
onClick={isConfigured ? onClick : undefined}
disabled={isDisabled}
aria-label="Enhance prompt with AI"
aria-label={t("common:enhancePromptWithAi")}
aria-busy={isLoading}
data-testid="enhance-prompt-button"
>
+119 -76
View File
@@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { cn } from "@/lib/utils";
import { listDirectory, type DirectoryListing } from "@/lib/api/domains/fs-api";
import { useTranslation } from "react-i18next";
type FolderPickerProps = {
/** Currently chosen absolute path; empty when nothing is picked. */
@@ -30,10 +31,11 @@ type FolderPickerProps = {
* trigger lives in the chip row and the popover handles browse + commit.
*/
export function FolderPicker({ value, onChange, placeholder }: FolderPickerProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { listing, loading, error, load } = useDirectoryListing(open, value);
const leaf = leafName(value);
const triggerLabel = leaf || placeholder || "scratch workspace";
const triggerLabel = leaf || placeholder || t("common:scratchWorkspace");
const hasValue = !!value;
const triggerClass = cn(
@@ -106,27 +108,31 @@ function leafName(path: string): string {
}
export function useDirectoryListing(open: boolean, value: string) {
const { t } = useTranslation();
const [listing, setListing] = useState<DirectoryListing | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestGeneration = useRef(0);
const load = useCallback(async (path: string) => {
const generation = ++requestGeneration.current;
setLoading(true);
setError(null);
setListing(null);
try {
const nextListing = await listDirectory(path);
if (generation !== requestGeneration.current) return;
setListing(nextListing);
} catch (err) {
if (generation !== requestGeneration.current) return;
setError(err instanceof Error ? err.message : "Failed to load directory");
} finally {
if (generation === requestGeneration.current) setLoading(false);
}
}, []);
const load = useCallback(
async (path: string) => {
const generation = ++requestGeneration.current;
setLoading(true);
setError(null);
setListing(null);
try {
const nextListing = await listDirectory(path);
if (generation !== requestGeneration.current) return;
setListing(nextListing);
} catch (err) {
if (generation !== requestGeneration.current) return;
setError(err instanceof Error ? err.message : t("common:failedToLoadDirectory"));
} finally {
if (generation === requestGeneration.current) setLoading(false);
}
},
[t],
);
useEffect(() => {
// Reset cached listing when the popover closes so the next open reloads
@@ -197,11 +203,12 @@ function Breadcrumb({
onNavigate: (p: string) => void;
touchRows?: boolean;
}) {
const { t } = useTranslation();
const segs = pathSegments(path);
return (
<div className="flex items-center gap-0.5 overflow-x-auto overflow-y-hidden border-b border-border bg-muted/30 px-2 py-1.5">
{segs.length === 0 && (
<span className="text-[11px] text-muted-foreground italic">Loading…</span>
<span className="text-[11px] text-muted-foreground italic">{t("common:loading")}</span>
)}
{segs.map((seg, i) => {
const last = i === segs.length - 1;
@@ -280,6 +287,7 @@ function DirectoryBrowserToolbar({
onCreateDirectory: (name: string) => Promise<void>;
touchRows: boolean;
}) {
const { t } = useTranslation();
const [editing, setEditing] = useState(false);
const [name, setName] = useState("");
const [creating, setCreating] = useState(false);
@@ -294,7 +302,7 @@ function DirectoryBrowserToolbar({
await onCreateDirectory(trimmedName);
setEditing(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create folder");
setError(err instanceof Error ? err.message : t("common:failedToCreateFolder"));
} finally {
setCreating(false);
}
@@ -303,60 +311,21 @@ function DirectoryBrowserToolbar({
return (
<div className="shrink-0 border-b border-border bg-muted/10 px-3 py-2">
{editing ? (
<div className="space-y-1.5">
<div className="flex min-w-0 items-center gap-2">
<IconFolderPlus className="size-4 shrink-0 text-muted-foreground" />
<Input
aria-label="New folder name"
value={name}
onChange={(event) => {
setName(event.target.value);
setError(null);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void createFolder();
} else if (event.key === "Escape") {
setEditing(false);
}
}}
placeholder="Folder name"
autoFocus
className={cn("min-w-0 flex-1", touchRows && "h-10")}
/>
<Button
type="button"
size={touchRows ? "icon-lg" : "icon"}
className={touchRows ? "size-11" : undefined}
onClick={() => void createFolder()}
disabled={!name.trim() || creating}
aria-label="Create folder"
title="Create folder"
>
<IconCheck />
</Button>
<Button
type="button"
variant="ghost"
size={touchRows ? "icon-lg" : "icon"}
className={touchRows ? "size-11" : undefined}
onClick={() => setEditing(false)}
aria-label="Cancel new folder"
title="Cancel"
>
<IconX />
</Button>
</div>
{error ? (
<p role="alert" className="pl-6 text-xs text-destructive">
{error}
</p>
) : null}
</div>
<NewFolderForm
name={name}
error={error}
creating={creating}
touchRows={touchRows}
onNameChange={(next) => {
setName(next);
setError(null);
}}
onSubmit={() => void createFolder()}
onCancel={() => setEditing(false)}
/>
) : (
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-medium text-muted-foreground">Folders</span>
<span className="text-xs font-medium text-muted-foreground">{t("common:folders")}</span>
<Button
type="button"
variant="ghost"
@@ -366,7 +335,7 @@ function DirectoryBrowserToolbar({
disabled={disabled}
>
<IconFolderPlus />
New folder
{t("common:newFolder")}
</Button>
</div>
)}
@@ -374,6 +343,78 @@ function DirectoryBrowserToolbar({
);
}
/** The inline "new folder" row, extracted to keep the toolbar under the
* max-lines-per-function cap. */
function NewFolderForm({
name,
error,
creating,
touchRows,
onNameChange,
onSubmit,
onCancel,
}: {
name: string;
error: string | null;
creating: boolean;
touchRows: boolean;
onNameChange: (next: string) => void;
onSubmit: () => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-1.5">
<div className="flex min-w-0 items-center gap-2">
<IconFolderPlus className="size-4 shrink-0 text-muted-foreground" />
<Input
aria-label={t("common:newFolderName")}
value={name}
onChange={(event) => onNameChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
onSubmit();
} else if (event.key === "Escape") {
onCancel();
}
}}
placeholder={t("common:folderName")}
autoFocus
className={cn("min-w-0 flex-1", touchRows && "h-10")}
/>
<Button
type="button"
size={touchRows ? "icon-lg" : "icon"}
className={touchRows ? "size-11" : undefined}
onClick={onSubmit}
disabled={!name.trim() || creating}
aria-label={t("common:createFolder")}
title={t("common:createFolder")}
>
<IconCheck />
</Button>
<Button
type="button"
variant="ghost"
size={touchRows ? "icon-lg" : "icon"}
className={touchRows ? "size-11" : undefined}
onClick={onCancel}
aria-label={t("common:cancelNewFolder")}
title={t("common:cancel")}
>
<IconX />
</Button>
</div>
{error ? (
<p role="alert" className="pl-6 text-xs text-destructive">
{error}
</p>
) : null}
</div>
);
}
function Entries({
listing,
loading,
@@ -389,10 +430,11 @@ function Entries({
touchRows?: boolean;
fillAvailableHeight?: boolean;
}) {
if (loading) return <EmptyRow text="Loading…" />;
const { t } = useTranslation();
if (loading) return <EmptyRow text={t("common:loading")} />;
if (error) return <EmptyRow text={error} variant="error" testId="folder-picker-error" />;
if (!listing || listing.entries.length === 0) {
return <EmptyRow text="No folders here — pick this one or go up" />;
return <EmptyRow text={t("common:noFoldersHerePickThisOne")} />;
}
return (
<div
@@ -445,6 +487,7 @@ function Footer({
onUseScratch: () => void;
onChoose: () => void;
}) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-between gap-2 border-t border-border bg-muted/20 px-2 py-1.5">
<button
@@ -454,7 +497,7 @@ function Footer({
className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground cursor-pointer"
>
<IconBox className="h-3 w-3" />
Use scratch instead
{t("common:useScratchInstead")}
</button>
<button
type="button"
@@ -468,7 +511,7 @@ function Footer({
)}
>
<IconFolder className="h-3 w-3" />
Use this folder
{t("common:useThisFolder")}
</button>
</div>
);
+7 -1
View File
@@ -1,12 +1,18 @@
"use client";
import { useTranslation } from "react-i18next";
type GridSpinnerProps = {
className?: string;
};
export function GridSpinner({ className }: GridSpinnerProps) {
const { t } = useTranslation();
return (
<span className={`spinner-grid ${className ?? ""}`} role="status" aria-label="Loading">
<span
className={`spinner-grid ${className ?? ""}`}
role="status"
aria-label={t("common:loadingIndicatorLabel")}
>
<span className="spinner-grid-cube" />
<span className="spinner-grid-cube" />
<span className="spinner-grid-cube" />
@@ -26,6 +26,8 @@ import {
resolveImproveKandevWorkflow,
type ImproveKandevKind,
} from "./improve-kandev-dialog-model";
import { Trans, useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
export type BootstrapState =
| { kind: "idle" }
@@ -43,22 +45,22 @@ type ImproveKind = ImproveKandevKind;
// Surfaced in the dialog's submit-button tooltip while the bootstrap probe is
// in flight, so the disabled state has an explanation the user can act on
// instead of the usual missing-field reasons.
function bootstrapBlockedReason(bootstrap: BootstrapState): string | null {
function bootstrapBlockedReason(t: TFunction, bootstrap: BootstrapState): string | null {
if (bootstrap.kind === "loading" || bootstrap.kind === "idle") {
return "Preparing kandev repository…";
return t("common:preparingKandevRepository");
}
if (bootstrap.kind === "error") {
return "Bootstrap failed — close and reopen to retry.";
return t("common:bootstrapFailedCloseAndReopen");
}
return null;
}
const PLACEHOLDERS: Record<ImproveKind, string> = {
bug: "E.g. When I open the kanban board, the column header text overlaps the task count badge on screens narrower than 1200px...",
feature:
"E.g. Add a keyboard shortcut to mark a task as Done from the task detail view, and show the shortcut in the task action menu...",
issue:
"Describe the problem or request you want published. The agent will ask for any details required by the matching GitHub issue template...",
// Keys, not resolved copy: a `t()` at module scope would freeze at the boot
// locale and the pseudo-locale could not see it.
const PLACEHOLDER_KEYS: Record<ImproveKind, string> = {
bug: "common:improveKandevBugPlaceholder",
feature: "common:improveKandevFeaturePlaceholder",
issue: "common:improveKandevIssuePlaceholder",
};
type CreateModeViewProps = {
@@ -74,6 +76,7 @@ type CreateModeViewProps = {
};
export function CreateModeView(props: CreateModeViewProps) {
const { t } = useTranslation();
const { bootstrap, captureLogs, setCaptureLogs } = props;
const [kind, setKind] = useState<ImproveKind>("bug");
const ready = bootstrap.kind === "ready" ? bootstrap : null;
@@ -104,9 +107,9 @@ export function CreateModeView(props: CreateModeViewProps) {
onSuccess={props.onTaskCreated}
lockedFields={{ repository: true, branch: true, workflow: true }}
submitBlockedReason={
props.externalBlockedReason ?? forkBlockedReason ?? bootstrapBlockedReason(bootstrap)
props.externalBlockedReason ?? forkBlockedReason ?? bootstrapBlockedReason(t, bootstrap)
}
descriptionPlaceholder={PLACEHOLDERS[kind]}
descriptionPlaceholder={t(PLACEHOLDER_KEYS[kind])}
aboveDescriptionSlot={<KindTabs kind={kind} onChange={handleKindChange} />}
extraFormSlot={
<BootstrapStatusSlot
@@ -138,12 +141,13 @@ function CreateModeBottomSlot({ kind, steps }: { kind: ImproveKind; steps: Workf
}
function WorkflowStepsPreview({ steps }: { steps: WorkflowStep[] }) {
const { t } = useTranslation();
if (steps.length === 0) return null;
const ordered = [...steps].sort((a, b) => a.position - b.position);
return (
<div className="rounded-md border border-border bg-muted/30 px-3 py-2">
<p className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80">
Workflow
{t("common:workflow")}
</p>
<TooltipProvider delayDuration={150}>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
@@ -175,6 +179,7 @@ function WorkflowStepsPreview({ steps }: { steps: WorkflowStep[] }) {
}
function UsefulInfoCollapsible() {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
return (
<Collapsible open={open} onOpenChange={setOpen}>
@@ -184,7 +189,7 @@ function UsefulInfoCollapsible() {
className="group flex w-full cursor-pointer items-center justify-between rounded-md border border-border bg-muted/30 px-3 py-2 text-left text-xs hover:bg-muted/50"
>
<span className="font-medium text-muted-foreground">
Useful commands &amp; agent skills
{t("common:usefulCommandsAgentSkills")}
</span>
<IconChevronDown className="h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</button>
@@ -192,32 +197,33 @@ function UsefulInfoCollapsible() {
<CollapsibleContent>
<div className="mt-2 space-y-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-relaxed text-muted-foreground">
<p>
Shell command you can run in the secondary instance, plus slash-command <em>skills</em>{" "}
you can ask the agent to run during the workflow.
<Trans i18nKey="common:shellCommandPlusSlashCommandSkills">
Shell command you can run in the secondary instance, plus slash-command{" "}
<em>skills</em> you can ask the agent to run during the workflow.
</Trans>
</p>
<div className="space-y-2">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
Shell
{t("common:shell")}
</p>
<UsefulInfoItem cmd="make install && make dev">
Boots a secondary kandev dev instance with a clean DB so you can verify the change
without touching your main one.
{t("common:bootsASecondaryKandevDevInstance")}
</UsefulInfoItem>
</div>
<div className="space-y-2">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
Agent skills (ask the agent to run)
{t("common:agentSkillsAskTheAgentTo")}
</p>
<UsefulInfoItem cmd="/commit">
Stages and commits changes using Conventional Commits.
{t("common:stagesAndCommitsChangesUsingConventional")}
</UsefulInfoItem>
<UsefulInfoItem cmd="/push">Commits and pushes to the current branch.</UsefulInfoItem>
<UsefulInfoItem cmd="/push">{t("common:commitsAndPushesToTheCurrent")}</UsefulInfoItem>
<UsefulInfoItem cmd="/verify">
Runs format, typecheck, test, and lint across the monorepo.
{t("common:runsFormatTypecheckTestAndLint")}
</UsefulInfoItem>
<UsefulInfoItem cmd="/pr">Commits, pushes, and opens a pull request.</UsefulInfoItem>
<UsefulInfoItem cmd="/pr">{t("common:commitsPushesAndOpensAPull")}</UsefulInfoItem>
<UsefulInfoItem cmd="/pr-fixup">
Waits for CI and automated reviews, then fixes failures and addresses comments.
{t("common:waitsForCiAndAutomatedReviews")}
</UsefulInfoItem>
</div>
</div>
@@ -242,17 +248,18 @@ function KindTabs({
kind: ImproveKind;
onChange: (next: ImproveKind) => void;
}) {
const { t } = useTranslation();
return (
<Tabs value={kind} onValueChange={(v) => onChange(v as ImproveKind)}>
<TabsList>
<TabsTrigger value="bug" className="cursor-pointer">
Bug fix
{t("common:bugFix")}
</TabsTrigger>
<TabsTrigger value="feature" className="cursor-pointer">
Feature request
{t("common:featureRequest")}
</TabsTrigger>
<TabsTrigger value="issue" className="cursor-pointer">
Open issue
{t("common:openIssue")}
</TabsTrigger>
</TabsList>
</Tabs>
@@ -270,6 +277,7 @@ function BootstrapStatusSlot({
captureLogs: boolean;
setCaptureLogs: (v: boolean) => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-2">
<BootstrapBanner bootstrap={bootstrap} kind={kind} />
@@ -278,32 +286,27 @@ function BootstrapStatusSlot({
className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-muted-foreground"
data-testid="improve-kandev-issue-only-notice"
>
This workflow will only create a GitHub issue from the repository template. The agent will
ask for missing details and publication approval; it will not implement the change or open
a pull request.
{t("common:thisWorkflowWillOnlyCreateA")}
</div>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<label className="flex cursor-pointer items-center gap-2">
<Checkbox checked={captureLogs} onCheckedChange={(v) => setCaptureLogs(v === true)} />
Include recent backend &amp; browser logs as context for the agent
{t("common:includeRecentBackendAndBrowserLogs")}
</label>
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="How log context works"
aria-label={t("common:howLogContextWorks")}
className="cursor-help text-muted-foreground/70 hover:text-muted-foreground"
>
<IconInfoCircle className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-xs leading-relaxed">
Kandev keeps a small in-memory ring buffer of the most recent backend logs and browser
console events. When enabled, those logs are written to a temporary folder on your
machine, and the file paths are appended to the task description so the agent can read
them while investigating.
{t("common:kandevKeepsASmallInMemory")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -313,11 +316,12 @@ function BootstrapStatusSlot({
}
function BootstrapBanner({ bootstrap, kind }: { bootstrap: BootstrapState; kind: ImproveKind }) {
const { t } = useTranslation();
if (bootstrap.kind === "loading" || bootstrap.kind === "idle") {
return (
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
Preparing kandev repository in background. Fill in the details, submit when ready.
{t("common:preparingKandevRepositoryInBackground")}
</div>
);
}
@@ -339,12 +343,13 @@ function ContributorBanner({
data: ImproveKandevBootstrapResponse;
kind: ImproveKind;
}) {
const { t } = useTranslation();
const { github_login: login, has_write_access: hasWrite } = data;
if (data.fork_status === "blocked_emu" && kind !== "issue") {
return (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
<IconAlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span>{data.fork_message ?? "This GitHub account cannot fork kdlbs/kandev."}</span>
<span>{data.fork_message ?? t("common:thisGithubAccountCannotForkKdlbs")}</span>
</div>
);
}
@@ -356,14 +361,18 @@ function ContributorBanner({
) : (
<IconGitFork className="h-3.5 w-3.5 mt-0.5 shrink-0" />
)}
{/* One complete sentence per branch. Splitting this into a "Contributing
as" stem plus a tail would freeze the word order in JSX and leave the
tail untranslatable. */}
<span>
Contributing as{" "}
{login ? (
<code className="font-mono text-foreground">@{login}</code>
<Trans i18nKey="common:contributingAsLogin" values={{ login, access: accessMessage }}>
Contributing as <code className="font-mono text-foreground">@{login}</code>.{" "}
{accessMessage}
</Trans>
) : (
<span>your GitHub account</span>
t("common:contributingAsYourGithubAccount", { access: accessMessage })
)}
. {accessMessage}
</span>
</div>
);
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { Trans } from "react-i18next";
/**
* These sentences interleave markup with a value, so the codemod half-converted
* them: it keyed the `<code>` contents (`gh`, `kdlbs/kandev`) and left the prose
* around them as a JSX literal. That renders correctly in English and passes
* lint, `i18n:check`, `i18n:ratchet` and `i18n:removed` — the word order is just
* frozen in JSX where no translator can reach it.
*
* Rebuilt as one `<Trans>` each. Two things are easy to get wrong and invisible
* afterwards, so they are pinned here:
*
* 1. The `<n>` index must land on the element's position among ALL children,
* counting text and `{" "}` expression containers. Prettier reflowing a
* line inserts a `{" "}` and silently shifts every index after it.
* 2. `gh` and `kdlbs/kandev` travel through `values`, not as tag content, so
* a translator cannot reword a binary name or a repo slug. They cannot
* instead be left as bare children under an empty `<1></1>` tag — that
* form drops plain text children (it only preserves element children),
* which silently renders "needs the CLI".
*/
describe("improve-kandev dialog <Trans> copy", () => {
it("renders the gh-auth notice byte-identically to the old literal", () => {
const { container } = render(
<Trans
i18nKey="common:theFinalStepOpensAPullRequest"
values={{ binary: "gh", message: "Run gh auth login." }}
>
The final step of this workflow opens a pull request, which needs the <code>gh</code> CLI to
be authenticated. {"Run gh auth login."}
</Trans>,
);
expect(container.textContent).toBe(
"The final step of this workflow opens a pull request, which needs the gh CLI to be " +
"authenticated. Run gh auth login.",
);
expect(container.querySelector("code")?.textContent).toBe("gh");
});
it("renders the fork bullet with the repo slug intact", () => {
const { container } = render(
<Trans i18nKey="common:theAgentForksKandevToYourAccount" values={{ repo: "kdlbs/kandev" }}>
The agent forks <code>kdlbs/kandev</code> to your GitHub account and opens a PR from your
fork, credited to you
</Trans>,
);
expect(container.textContent).toBe(
"The agent forks kdlbs/kandev to your GitHub account and opens a PR from your fork, " +
"credited to you",
);
expect(container.querySelector("code")?.textContent).toBe("kdlbs/kandev");
});
it("keeps the contributor line one sentence per branch", () => {
const { container } = render(
<Trans
i18nKey="common:contributingAsLogin"
values={{ login: "octocat", access: "You have write access." }}
>
Contributing as <code>@octocat</code>. {"You have write access."}
</Trans>,
);
expect(container.textContent).toBe("Contributing as @octocat. You have write access.");
});
});
+49 -23
View File
@@ -22,6 +22,14 @@ import {
readImproveKandevSkipIntro,
writeImproveKandevSkipIntro,
} from "./improve-kandev-dialog-model";
import { Trans, useTranslation } from "react-i18next";
// The module-level `t` — resolved when called, not at import — for the two
// error-only strings below. The hook-provided `t` changes identity on a locale
// switch, and putting it in these effects' dependency arrays would re-POST
// `bootstrapImproveKandev` (which forks a repository) and reset auth state
// whenever the user changes language. Render-time copy still uses the reactive
// `t` from `useTranslation()`.
import { t as tStatic } from "@/lib/i18n";
type ImproveKandevDialogProps = {
open: boolean;
@@ -38,6 +46,7 @@ type AuthState =
| { kind: "missing"; message: string; fixUrl: string; fixLabel: string };
export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
const { t } = useTranslation();
const { open, onOpenChange, workspaceId, onSuccess } = props;
const [mode, setMode] = useState<Mode>(() => initialImproveKandevMode(readSkipIntro()));
const [skipIntro, setSkipIntro] = useState(() => readSkipIntro());
@@ -95,7 +104,9 @@ export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
setCaptureLogs={setCaptureLogs}
transformDescription={transformDescription}
onTaskCreated={handleSuccess}
externalBlockedReason={auth.kind === "checking" ? "Checking GitHub authentication…" : null}
externalBlockedReason={
auth.kind === "checking" ? t("common:checkingGithubAuthentication") : null
}
/>
);
}
@@ -106,7 +117,7 @@ export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<IconStethoscope className="h-5 w-5" />
Improve Kandev
{t("common:improveKandev")}
</DialogTitle>
</DialogHeader>
<IntroBody
@@ -145,7 +156,7 @@ function useGitHubAuthCheck(
kind: "missing",
message: ghIssue.message,
fixUrl: ghIssue.fix_url.replace("{workspaceId}", workspaceId),
fixLabel: ghIssue.fix_label || "Configure GitHub",
fixLabel: ghIssue.fix_label || tStatic("common:configureGithub"),
});
} catch {
if (!cancelled) setAuth({ kind: "ok" }); // Fail open — bootstrap will surface real errors.
@@ -190,10 +201,10 @@ function useBootstrapKandev(
});
} catch (err) {
if (cancelled) return;
const message = err instanceof Error ? err.message : "Bootstrap failed";
const message = err instanceof Error ? err.message : tStatic("common:bootstrapFailed");
setBootstrap({ kind: "error", message });
toast({
title: "Could not prepare Improve Kandev",
title: tStatic("common:couldNotPrepareImproveKandev"),
description: message,
variant: "error",
});
@@ -239,21 +250,29 @@ function GhAuthMissing({
auth: Extract<AuthState, { kind: "missing" }>;
onCancel: () => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-4 py-2">
<div className="flex items-start gap-3 rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
<IconAlertTriangle className="h-4 w-4 shrink-0 text-amber-500" />
<div>
<p className="font-medium text-foreground">GitHub CLI not authenticated</p>
<p className="font-medium text-foreground">{t("common:githubCliNotAuthenticated")}</p>
<p className="mt-1 text-muted-foreground">
The final step of this workflow opens a pull request, which needs the <code>gh</code>{" "}
CLI to be authenticated. {auth.message}
{/* `gh` is the CLI's binary name, so it stays literal in the children
and never becomes a catalog key. */}
<Trans
i18nKey="common:theFinalStepOpensAPullRequest"
values={{ message: auth.message }}
>
The final step of this workflow opens a pull request, which needs the <code>gh</code>{" "}
CLI to be authenticated. {auth.message}
</Trans>
</p>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel} className="cursor-pointer">
Cancel
{t("common:cancel")}
</Button>
<Button asChild className="cursor-pointer">
<Link href={auth.fixUrl} onClick={onCancel}>
@@ -278,30 +297,37 @@ function IntroExplanation({
onCancel: () => void;
onProceed: () => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-5 py-2">
<p className="text-sm leading-relaxed text-muted-foreground">
Kandev is open source, and you can help make it better.
{t("common:kandevIsOpenSourceAndYou")}
</p>
<p className="text-sm leading-relaxed text-muted-foreground">
Describe a bug you hit or a feature you&apos;d like, and we&apos;ll create a task on your
own agent to implement it in the kandev codebase.
{t("common:describeABugYouHitOr")}
</p>
<p className="text-sm leading-relaxed text-muted-foreground">
When it&apos;s done, the agent opens a pull request to{" "}
<code className="font-mono text-xs">kdlbs/kandev</code> for the maintainers to review,
saving them time and shipping the improvement to everyone.
<Trans i18nKey="common:whenItsDoneTheAgentOpensAPr" values={{ repo: "kdlbs/kandev" }}>
When it&apos;s done, the agent opens a pull request to{" "}
<code className="font-mono text-xs">kdlbs/kandev</code> for the maintainers to review,
saving them time and shipping the improvement to everyone.
</Trans>
</p>
<ul className="space-y-2 text-sm text-muted-foreground">
<IntroBullet>Create a task describing your bug or feature request</IntroBullet>
<IntroBullet>Your agent implements it in the kandev repository, with tests</IntroBullet>
<IntroBullet>You verify and test the change in a second kandev instance</IntroBullet>
<IntroBullet>{t("common:createATaskDescribingYourBug")}</IntroBullet>
<IntroBullet>{t("common:yourAgentImplementsItInThe")}</IntroBullet>
<IntroBullet>{t("common:youVerifyAndTestTheChange")}</IntroBullet>
<IntroBullet>
The agent forks <code className="font-mono text-xs">kdlbs/kandev</code> to your GitHub
account and opens a PR from your fork, credited to you
<Trans
i18nKey="common:theAgentForksKandevToYourAccount"
values={{ repo: "kdlbs/kandev" }}
>
The agent forks <code className="font-mono text-xs">kdlbs/kandev</code> to your GitHub
account and opens a PR from your fork, credited to you
</Trans>
</IntroBullet>
</ul>
<label
@@ -312,11 +338,11 @@ function IntroExplanation({
checked={skipIntro}
onCheckedChange={(checked) => onSkipIntroChange(checked === true)}
/>
Do not show this again
{t("common:doNotShowThisAgain")}
</label>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel} className="cursor-pointer">
Cancel
{t("common:cancel")}
</Button>
<Button
onClick={onProceed}
@@ -324,7 +350,7 @@ function IntroExplanation({
className="cursor-pointer"
data-testid="improve-kandev-proceed"
>
Contribute
{t("common:contribute")}
</Button>
</div>
</div>
+76 -51
View File
@@ -34,6 +34,7 @@ import { listAvailableAgents, listWorkflowTemplates } from "@/lib/api";
import { listAgentsAction, updateAgentProfileAction } from "@/app/actions/agents";
import { StepAgents, type AgentSetting } from "@/components/onboarding/step-agents";
import type { AvailableAgent, ToolStatus, WorkflowTemplate, AgentProfile } from "@/lib/types/http";
import { Trans, useTranslation } from "react-i18next";
interface OnboardingDialogProps {
open: boolean;
@@ -42,36 +43,47 @@ interface OnboardingDialogProps {
const TOTAL_STEPS = 4;
// Catalog keys, not resolved copy: `t()` at module scope would freeze at the
// boot locale. `id` stays untranslated so the React key is locale-independent.
const RUNTIMES = [
{
name: "Local",
description: "Run agents directly on your machine with full access to your local filesystem.",
id: "local",
nameKey: "common:runtimeLocal",
descriptionKey: "common:runtimeLocalDescription",
icon: IconFolder,
},
{
name: "Git Worktree",
description: "Isolated branch environment under a worktree root for parallel work.",
id: "worktree",
nameKey: "common:runtimeGitWorktree",
descriptionKey: "common:runtimeGitWorktreeDescription",
icon: IconFolders,
},
{
name: "Docker",
description: "Containerized execution for full isolation and reproducibility.",
id: "docker",
nameKey: "common:runtimeDocker",
descriptionKey: "common:runtimeDockerDescription",
icon: IconBrandDocker,
},
{
name: "Sprites (Remote sprites.dev)",
description: "Hardware-isolated execution environment for arbitrary code.",
id: "sprites",
nameKey: "common:runtimeSprites",
descriptionKey: "common:runtimeSpritesDescription",
icon: IconCloud,
href: "https://sprites.dev",
},
];
const STEP_TITLES = ["AI Agents", "Executors", "Agentic Workflows", "Command Panel"];
const STEP_DESCRIPTIONS = [
"Manage discovered agents and install new ones.",
"Agents can run in different executor environments — local, containerized, or remote.",
"Workflows define the steps and automation for your tasks.",
"Quick access to actions from anywhere with a keyboard shortcut.",
const STEP_TITLE_KEYS = [
"common:onboardingStepAgentsTitle",
"common:onboardingStepExecutorsTitle",
"common:onboardingStepWorkflowsTitle",
"common:onboardingStepCommandPanelTitle",
];
const STEP_DESCRIPTION_KEYS = [
"common:onboardingStepAgentsDescription",
"common:onboardingStepExecutorsDescription",
"common:onboardingStepWorkflowsDescription",
"common:onboardingStepCommandPanelDescription",
];
function buildAgentSettings(
@@ -215,29 +227,30 @@ function useOnboardingResources(open: boolean) {
}
function OnboardingFooter({ step, onSkip, onBack, onNext, onGetStarted }: OnboardingFooterProps) {
const { t } = useTranslation();
return (
<DialogFooter>
<div className="flex w-full items-center justify-between">
<Button variant="ghost" size="sm" onClick={onSkip} className="cursor-pointer">
<IconX className="mr-1.5 h-3.5 w-3.5" />
Skip
{t("common:skip")}
</Button>
<div className="flex gap-2">
{step > 0 && (
<Button variant="outline" onClick={onBack} className="cursor-pointer">
<IconArrowLeft className="mr-1.5 h-4 w-4" />
Back
{t("common:back")}
</Button>
)}
{step < TOTAL_STEPS - 1 ? (
<Button onClick={onNext} className="cursor-pointer">
Next
{t("common:next")}
<IconArrowRight className="ml-1.5 h-4 w-4" />
</Button>
) : (
<Button onClick={onGetStarted} className="cursor-pointer">
<IconCheck className="mr-1.5 h-4 w-4" />
Get Started
{t("common:getStarted")}
</Button>
)}
</div>
@@ -247,6 +260,7 @@ function OnboardingFooter({ step, onSkip, onBack, onNext, onGetStarted }: Onboar
}
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
const { t } = useTranslation();
const [step, setStep] = useState(0);
const {
availableAgents,
@@ -305,8 +319,10 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
<Dialog open={open} onOpenChange={() => {}}>
<DialogContent className="sm:max-w-3xl" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="text-center text-2xl">{STEP_TITLES[step]}</DialogTitle>
<DialogDescription className="text-center">{STEP_DESCRIPTIONS[step]}</DialogDescription>
<DialogTitle className="text-center text-2xl">{t(STEP_TITLE_KEYS[step])}</DialogTitle>
<DialogDescription className="text-center">
{t(STEP_DESCRIPTION_KEYS[step])}
</DialogDescription>
</DialogHeader>
<div className="py-4 min-h-[220px]">
{step === 0 && (
@@ -336,6 +352,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
}
function StepEnvironments() {
const { t } = useTranslation();
return (
<div className="space-y-3">
<div className="grid gap-2">
@@ -348,26 +365,26 @@ function StepEnvironments() {
rel="noopener noreferrer"
className="text-sm font-medium hover:underline cursor-pointer"
>
{runtime.name}
{t(runtime.nameKey)}
</a>
) : (
<p className="text-sm font-medium">{runtime.name}</p>
<p className="text-sm font-medium">{t(runtime.nameKey)}</p>
);
return (
<div key={runtime.name} className="flex items-start gap-3 rounded-lg border p-3">
<div key={runtime.id} className="flex items-start gap-3 rounded-lg border p-3">
<div className="h-8 w-8 rounded-md bg-muted flex items-center justify-center flex-shrink-0">
<Icon className="h-4.5 w-4.5 text-muted-foreground" />
</div>
<div className="min-w-0">
{nameEl}
<p className="text-xs text-muted-foreground">{runtime.description}</p>
<p className="text-xs text-muted-foreground">{t(runtime.descriptionKey)}</p>
</div>
</div>
);
})}
</div>
<p className="text-xs text-muted-foreground">
Configure executors in Settings to control where agents execute.
{t("common:configureExecutorsInSettingsToControl")}
</p>
</div>
);
@@ -380,11 +397,12 @@ function StepWorkflows({
templates: WorkflowTemplate[];
loading: boolean;
}) {
const { t } = useTranslation();
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-muted-foreground">
<IconLoader2 className="h-6 w-6 animate-spin" />
Loading workflow templates...
{t("common:loadingWorkflowTemplates")}
</div>
);
}
@@ -398,7 +416,7 @@ function StepWorkflows({
{defaultTemplate && <TemplateCard template={defaultTemplate} isDefault />}
{otherTemplates.length > 0 && (
<>
<p className="text-xs text-muted-foreground mt-1">Available templates</p>
<p className="text-xs text-muted-foreground mt-1">{t("common:availableTemplates")}</p>
{otherTemplates.map((template) => (
<TemplateCard key={template.id} template={template} />
))}
@@ -406,22 +424,23 @@ function StepWorkflows({
)}
</div>
<p className="text-xs text-muted-foreground">
Workflows control the steps, automation, and agent behavior for your tasks. You can add more
workflows from Settings.
{t("common:workflowsControlTheStepsAutomationAnd")}
</p>
</div>
);
}
// `id` keeps the React key locale-independent; `labelKey` resolves at render.
const COMMAND_PANEL_PREVIEW_ITEMS = [
{ icon: IconSearch, label: "Search Tasks", trailing: "→" },
{ icon: IconHome, label: "Go to Home" },
{ icon: IconGitCommit, label: "Commit Changes" },
{ icon: IconArrowDown, label: "Pull" },
{ icon: IconTerminal2, label: "Add Terminal Panel" },
{ id: "search", icon: IconSearch, labelKey: "common:commandPreviewSearchTasks", trailing: "→" },
{ id: "home", icon: IconHome, labelKey: "common:commandPreviewGoToHome" },
{ id: "commit", icon: IconGitCommit, labelKey: "common:commandPreviewCommitChanges" },
{ id: "pull", icon: IconArrowDown, labelKey: "common:commandPreviewPull" },
{ id: "terminal", icon: IconTerminal2, labelKey: "common:commandPreviewAddTerminalPanel" },
];
function StepCommandPanel() {
const { t } = useTranslation();
return (
<div className="space-y-4">
{/* Mock command panel preview */}
@@ -429,7 +448,7 @@ function StepCommandPanel() {
{/* Search input */}
<div className="flex items-center gap-2 px-3 py-2 border-b">
<IconSearch className="h-3.5 w-3.5 text-muted-foreground/50" />
<span className="text-xs text-muted-foreground/50">Type a command...</span>
<span className="text-xs text-muted-foreground/50">{t("common:typeACommand")}</span>
</div>
{/* Sample commands */}
<div className="py-1">
@@ -437,11 +456,11 @@ function StepCommandPanel() {
const Icon = item.icon;
return (
<div
key={item.label}
key={item.id}
className="flex items-center gap-3 px-3 py-1.5 text-sm first:bg-muted/50"
>
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="flex-1 text-xs">{item.label}</span>
<span className="flex-1 text-xs">{t(item.labelKey)}</span>
{item.trailing && (
<span className="text-xs text-muted-foreground">{item.trailing}</span>
)}
@@ -454,34 +473,39 @@ function StepCommandPanel() {
<span className="inline-flex items-center gap-1">
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
<span className="text-[0.6rem]">Navigate</span>
<span className="text-[0.6rem]">{t("common:navigate")}</span>
</span>
<span className="inline-flex items-center gap-1">
<Kbd>↵</Kbd>
<span className="text-[0.6rem]">Select</span>
<span className="text-[0.6rem]">{t("common:select")}</span>
</span>
<span className="inline-flex items-center gap-1">
{/* A key name, not copy — it labels the physical key. */}
<Kbd>esc</Kbd>
<span className="text-[0.6rem]">Close</span>
<span className="text-[0.6rem]">{t("common:close")}</span>
</span>
</div>
</div>
{/* Shortcut hint */}
{/* One message with indexed tags rather than a "Press" stem and a tail:
the flex children keep their gap-2 spacing, and a translator can still
move the key chord to wherever the sentence needs it. */}
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
<span>Press</span>
<span className="inline-flex items-center gap-0.5">
<Kbd>
<IconCommand className="size-3" />
</Kbd>
<Kbd>K</Kbd>
</span>
<span>to open it anytime</span>
<Trans i18nKey="common:pressCmdKToOpenItAnytime">
<span>Press</span>
<span className="inline-flex items-center gap-0.5">
<Kbd>
<IconCommand className="size-3" />
</Kbd>
<Kbd>K</Kbd>
</span>
<span>to open it anytime</span>
</Trans>
</div>
<p className="text-xs text-muted-foreground">
Navigate between pages, search tasks, trigger git operations, and manage panels — all
without leaving the keyboard. Context-aware commands appear based on the active page.
{t("common:navigateBetweenPagesSearchTasksTrigger")}
</p>
</div>
);
@@ -494,6 +518,7 @@ function TemplateCard({
template: WorkflowTemplate;
isDefault?: boolean;
}) {
const { t } = useTranslation();
const steps = (template.default_steps ?? []).slice().sort((a, b) => a.position - b.position);
return (
@@ -505,7 +530,7 @@ function TemplateCard({
{isDefault && (
<span className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
<IconCheck className="h-3.5 w-3.5" />
Default
{t("common:defaultBadge")}
</span>
)}
</div>
@@ -3,6 +3,7 @@
import { Button } from "@kandev/ui/button";
import type { UtilityGenerationResult } from "@/hooks/use-utility-agent-generator";
import { useTranslation } from "react-i18next";
type PromptResultRecoveryProps = {
pendingResult: UtilityGenerationResult | null;
@@ -15,6 +16,7 @@ export function PromptResultRecovery({
onApply,
onCopy,
}: PromptResultRecoveryProps) {
const { t } = useTranslation();
if (!pendingResult) {
return null;
}
@@ -25,7 +27,7 @@ export function PromptResultRecovery({
data-testid="prompt-result-recovery"
className="flex flex-col gap-2 rounded-md border border-border/60 bg-muted/30 p-3"
>
<p className="text-sm text-muted-foreground">An enhanced prompt is available.</p>
<p className="text-sm text-muted-foreground">{t("common:anEnhancedPromptIsAvailable")}</p>
<div className="flex flex-wrap items-center gap-2">
<Button
type="button"
@@ -33,10 +35,10 @@ export function PromptResultRecovery({
className="cursor-pointer"
onClick={() => void onCopy()}
>
Copy
{t("common:copy")}
</Button>
<Button type="button" className="cursor-pointer" onClick={onApply}>
Apply
{t("common:apply")}
</Button>
</div>
</div>
+39 -13
View File
@@ -11,6 +11,7 @@ import {
IconGitPullRequest,
IconLoader2,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import {
DropdownMenu,
@@ -51,6 +52,21 @@ export type PrimaryButtonConfig = {
tooltip: string;
};
/**
* One complete sentence per action rather than a single frame with the action
* name interpolated. `primaryButtonConfig.label` is itself already translated,
* and a translated value dropped into a translated frame cannot be declined to
* agree with the surrounding grammar (genitive in German/Russian, and so on) —
* nor can the frame be reordered around it. Keyed off `primaryAction`, which is
* an untranslated discriminant.
*/
const PICK_REPOSITORY_KEYS: Record<"commit" | "push" | "pr" | "rebase", string> = {
commit: "integrations:pickARepositoryForCommit",
push: "integrations:pickARepositoryForPush",
pr: "integrations:pickARepositoryForCreatePr",
rebase: "integrations:pickARepositoryForRebase",
};
const ITEM_CLASS = "cursor-pointer gap-3";
const ICON_CLASS = "h-4 w-4 text-muted-foreground";
@@ -58,7 +74,8 @@ type ActionKey = "commit" | "push" | "pr" | "pull" | "rebase" | "merge" | "force
type ActionDef = {
key: ActionKey;
label: string;
/** Catalog key, resolved at render: `t()` here would freeze at the boot locale. */
labelKey: string;
icon: ReactNode;
/** Returns true when this action should be disabled for a given repo. */
disabledFor: (status: PerRepoStatus | undefined) => boolean;
@@ -68,49 +85,49 @@ type ActionDef = {
const ACTION_DEFS: ActionDef[] = [
{
key: "commit",
label: "Commit",
labelKey: "integrations:commit",
icon: <IconGitCommit className={ICON_CLASS} />,
disabledFor: (s) => !((s?.hasStaged ?? false) || (s?.hasUnstaged ?? false)),
invoke: (repo, cb) => cb.onCommit(repo),
},
{
key: "push",
label: "Push",
labelKey: "integrations:push",
icon: <IconCloudUpload className={ICON_CLASS} />,
disabledFor: (s) => (s?.ahead ?? 0) === 0,
invoke: (repo, cb) => cb.onPush(false, repo),
},
{
key: "pr",
label: "Create PR",
labelKey: "integrations:createPr",
icon: <IconGitPullRequest className={ICON_CLASS} />,
disabledFor: () => false,
invoke: (repo, cb) => cb.onPR(repo),
},
{
key: "pull",
label: "Pull",
labelKey: "integrations:pull",
icon: <IconCloudDownload className={ICON_CLASS} />,
disabledFor: () => false,
invoke: (repo, cb) => cb.onPull(repo),
},
{
key: "rebase",
label: "Rebase",
labelKey: "integrations:rebase",
icon: <IconGitCherryPick className={ICON_CLASS} />,
disabledFor: () => false,
invoke: (repo, cb) => cb.onRebase(repo),
},
{
key: "merge",
label: "Merge",
labelKey: "integrations:merge",
icon: <IconGitMerge className={ICON_CLASS} />,
disabledFor: () => false,
invoke: (repo, cb) => cb.onMerge(repo),
},
{
key: "force-push",
label: "Force Push",
labelKey: "integrations:forcePush",
icon: <IconAlertTriangle className={ICON_CLASS} />,
disabledFor: (s) => (s?.ahead ?? 0) === 0,
invoke: (repo, cb) => cb.onPush(true, repo),
@@ -138,19 +155,20 @@ function PerRepoActionSub({
repoDisplayName: (repositoryName: string) => string | undefined;
callbacks: PerRepoCallbacks;
}) {
const { t } = useTranslation();
const statusByName = new Map(perRepoStatus.map((s) => [s.repository_name, s]));
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger className={ITEM_CLASS} disabled={disabled}>
{action.icon}
<span className="flex-1">{action.label}</span>
<span className="flex-1">{t(action.labelKey)}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-52">
{repoNames.map((repo) => {
const status = statusByName.get(repo);
const ahead = status?.ahead ?? 0;
const behind = status?.behind ?? 0;
const label = repoDisplayName(repo) || repo || "Repository";
const label = repoDisplayName(repo) || repo || t("integrations:repository");
return (
<DropdownMenuItem
key={repo || "__no_repo__"}
@@ -194,10 +212,11 @@ function MultiRepoVcsDropdown({
repoDisplayName: (repositoryName: string) => string | undefined;
callbacks: PerRepoCallbacks;
}) {
const { t } = useTranslation();
return (
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="text-[10px] text-muted-foreground/70 uppercase tracking-wide">
Pick action, then repo
{t("integrations:pickActionThenRepo")}
</DropdownMenuLabel>
{ACTION_DEFS.map((action, idx) => (
<div key={action.key}>
@@ -221,10 +240,16 @@ function MultiRepoVcsDropdown({
}
/** Inline footnote under Rebase/Merge showing the target branch. */
// `type` stays a discriminant and never reaches the screen: rendering the union
// member directly would make "onto"/"from" untranslatable and couple the copy to
// the logic. Each branch resolves a whole message with the ref interpolated.
function RebaseMergeFootnote({ target, type }: { target: string; type: "onto" | "from" }) {
const { t } = useTranslation();
return (
<div className="px-3 py-0.5 text-[10px] text-muted-foreground/60">
{type} {target}
{type === "onto"
? t("integrations:ontoBranch", { branch: target })
: t("integrations:fromBranch", { branch: target })}
</div>
);
}
@@ -256,6 +281,7 @@ export function MultiRepoVcsButton({
repoDisplayName: (repositoryName: string) => string | undefined;
callbacks: PerRepoCallbacks;
}) {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -294,7 +320,7 @@ export function MultiRepoVcsButton({
</DropdownMenu>
</span>
</TooltipTrigger>
<TooltipContent>Pick a repository for {primaryButtonConfig.label}</TooltipContent>
<TooltipContent>{t(PICK_REPOSITORY_KEYS[primaryAction])}</TooltipContent>
</Tooltip>
);
}
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { t } from "@/lib/i18n";
/**
* The VCS tooltips used to build their plural by hand:
*
* `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`
*
* That puts the plural rule at the call site where no other locale can reach
* it, so the copy moved to `count` + `_one`/`_other`. These assertions pin the
* English to exactly what the concatenation emitted — E2E specs and the
* accessible-name queries depend on it, and a plural key is the easiest place
* to silently reword a string while every gate stays green.
*/
describe("vcs split-button count-bearing copy", () => {
const legacyCommit = (n: number) => `Commit ${n} changed file${n !== 1 ? "s" : ""}`;
const legacyPr = (n: number) => `Create PR (${n} commit${n !== 1 ? "s" : ""} ahead)`;
const legacyPush = (n: number) => `Push ${n} commit${n !== 1 ? "s" : ""} to remote`;
// 1 is the only value the old ternary treated specially; 0 and 2 both take
// the "s" branch, and 0 is the one a naive `n > 1` port would get wrong.
for (const n of [0, 1, 2, 5]) {
it(`matches the pre-migration English for ${n} commit(s)`, () => {
expect(t("integrations:commitChangedFiles", { count: n })).toBe(legacyCommit(n));
expect(t("integrations:createPrCommitsAhead", { count: n })).toBe(legacyPr(n));
expect(t("integrations:pushCommitsToRemote", { count: n })).toBe(legacyPush(n));
});
}
it("interpolates the git ref rather than translating it", () => {
expect(t("integrations:ontoBranch", { branch: "origin/main" })).toBe("onto origin/main");
expect(t("integrations:fromBranch", { branch: "release/1.2" })).toBe("from release/1.2");
expect(t("integrations:rebaseOntoBranchBehind", { branch: "origin/main", behind: 3 })).toBe(
"Rebase onto origin/main (3 behind)",
);
});
it("keeps the divergence aria-labels count-invariant, as the shipped English was", () => {
expect(t("integrations:commitsAheadAriaLabel", { value: 1 })).toBe("1 commits ahead");
expect(t("integrations:commitsBehindAriaLabel", { value: 2 })).toBe("2 commits behind");
});
});
/**
* The discard dialog used to concatenate a " in your local clone at X" tail
* onto a shared stem, which froze the tail's position in the sentence. Each
* branch is now a whole message; these pin both to the previous rendering.
*/
describe("discard-local-changes description", () => {
it("reads identically to the old stem + tail concatenation", () => {
const stem = "Starting this task will permanently discard the uncommitted changes";
const tail = "Back up anything you want to keep before continuing.";
expect(t("common:discardLocalChangesDescription")).toBe(`${stem} in your local clone. ${tail}`);
expect(t("common:discardLocalChangesAtPathDescription", { repoPath: "/srv/repo" })).toBe(
`${stem} in your local clone at /srv/repo. ${tail}`,
);
});
});
+75 -30
View File
@@ -1,6 +1,8 @@
"use client";
import { memo, useCallback, type ComponentProps, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import {
IconGitCommit,
IconGitPullRequest,
@@ -55,57 +57,75 @@ type PrimaryButtonConfig = {
onClick: () => void;
};
// The count-bearing tooltips go through `count` + `_one`/`_other` rather than a
// concatenated `commit${n !== 1 ? "s" : ""}`: an English morpheme appended here
// puts the plural rule at the call site, where no other locale can reach it.
function buildCommitConfig(
t: TFunction,
uncommittedFileCount: number,
openCommitDialog: () => void,
): PrimaryButtonConfig {
return {
icon: <IconGitCommit className="h-4 w-4" />,
label: "Commit",
label: t("integrations:commit"),
badge: uncommittedFileCount > 0 ? uncommittedFileCount : null,
tooltip:
uncommittedFileCount > 0
? `Commit ${uncommittedFileCount} changed file${uncommittedFileCount !== 1 ? "s" : ""}`
: "No changes to commit",
? t("integrations:commitChangedFiles", { count: uncommittedFileCount })
: t("integrations:noChangesToCommit"),
onClick: openCommitDialog,
};
}
function buildPrConfig(aheadCount: number, openPRDialog: () => void): PrimaryButtonConfig {
function buildPrConfig(
t: TFunction,
aheadCount: number,
openPRDialog: () => void,
): PrimaryButtonConfig {
return {
icon: <IconGitPullRequest className="h-4 w-4" />,
label: "Create PR",
label: t("integrations:createPr"),
badge: null,
tooltip: `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`,
tooltip: t("integrations:createPrCommitsAhead", { count: aheadCount }),
onClick: openPRDialog,
};
}
function buildPushConfig(aheadCount: number, handlePush: () => void): PrimaryButtonConfig {
function buildPushConfig(
t: TFunction,
aheadCount: number,
handlePush: () => void,
): PrimaryButtonConfig {
return {
icon: <IconCloudUpload className="h-4 w-4" />,
label: "Push",
label: t("integrations:push"),
badge: null,
tooltip: `Push ${aheadCount} commit${aheadCount !== 1 ? "s" : ""} to remote`,
tooltip: t("integrations:pushCommitsToRemote", { count: aheadCount }),
onClick: handlePush,
};
}
function buildRebaseConfig(
t: TFunction,
behindCount: number,
baseBranch: string | undefined,
handleRebase: () => void,
): PrimaryButtonConfig {
return {
icon: <IconGitCherryPick className="h-4 w-4" />,
label: "Rebase",
label: t("integrations:rebase"),
badge: behindCount > 0 ? behindCount : null,
tooltip: `Rebase onto ${baseBranch || DEFAULT_BASE_BRANCH} (${behindCount} behind)`,
// `branch` is a git ref — data, interpolated rather than translated.
tooltip: t("integrations:rebaseOntoBranchBehind", {
branch: baseBranch || DEFAULT_BASE_BRANCH,
behind: behindCount,
}),
onClick: handleRebase,
};
}
type PrimaryConfigArgs = {
t: TFunction;
primaryAction: "commit" | "push" | "pr" | "rebase";
uncommittedFileCount: number;
aheadCount: number;
@@ -118,6 +138,7 @@ type PrimaryConfigArgs = {
};
function buildPrimaryButtonConfig({
t,
primaryAction,
uncommittedFileCount,
aheadCount,
@@ -128,10 +149,11 @@ function buildPrimaryButtonConfig({
handlePush,
handleRebase,
}: PrimaryConfigArgs): PrimaryButtonConfig {
if (primaryAction === "push") return buildPushConfig(aheadCount, handlePush);
if (primaryAction === "pr") return buildPrConfig(aheadCount, openPRDialog);
if (primaryAction === "rebase") return buildRebaseConfig(behindCount, baseBranch, handleRebase);
return buildCommitConfig(uncommittedFileCount, openCommitDialog);
if (primaryAction === "push") return buildPushConfig(t, aheadCount, handlePush);
if (primaryAction === "pr") return buildPrConfig(t, aheadCount, openPRDialog);
if (primaryAction === "rebase")
return buildRebaseConfig(t, behindCount, baseBranch, handleRebase);
return buildCommitConfig(t, uncommittedFileCount, openCommitDialog);
}
type DivergenceTone = "ahead" | "behind";
@@ -144,17 +166,17 @@ const divergenceToneClass: Record<DivergenceTone, string> = {
function DivergencePill({
tone,
value,
label,
ariaLabel,
}: {
tone: DivergenceTone;
value: number;
label: string;
ariaLabel: string;
}) {
if (value <= 0) return null;
return (
<span
aria-label={`${value} ${label}`}
aria-label={ariaLabel}
className={cn(
"inline-flex h-5 items-center rounded-md border px-1.5 text-[11px] font-semibold leading-none tabular-nums",
divergenceToneClass[tone],
@@ -167,12 +189,24 @@ function DivergencePill({
}
function GitDivergencePills({ ahead, behind }: { ahead: number; behind: number }) {
const { t } = useTranslation();
if (ahead <= 0 && behind <= 0) return null;
// `{{value}}`, not `{{count}}`: the shipped English is count-invariant
// ("1 commits ahead"), and switching to a plural here would change the
// accessible name that E2E specs query by.
return (
<span className="ml-1 inline-flex items-center gap-1">
<DivergencePill tone="ahead" value={ahead} label="commits ahead" />
<DivergencePill tone="behind" value={behind} label="commits behind" />
<DivergencePill
tone="ahead"
value={ahead}
ariaLabel={t("integrations:commitsAheadAriaLabel", { value: ahead })}
/>
<DivergencePill
tone="behind"
value={behind}
ariaLabel={t("integrations:commitsBehindAriaLabel", { value: behind })}
/>
</span>
);
}
@@ -202,16 +236,17 @@ function VcsDropdownItems({
onRebase,
onMerge,
}: VcsDropdownItemsProps) {
const { t } = useTranslation();
return (
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onPR} disabled={disabled}>
<IconGitPullRequest className="h-4 w-4 text-muted-foreground" />
<span className="flex-1">Create PR</span>
<span className="flex-1">{t("integrations:createPr")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onPull} disabled={disabled}>
<IconCloudDownload className="h-4 w-4 text-muted-foreground" />
<span className="flex-1">Pull</span>
<span className="flex-1">{t("integrations:pull")}</span>
{hasMatchingUpstream && behindCount > 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
↓{behindCount}
@@ -221,7 +256,7 @@ function VcsDropdownItems({
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer gap-3" disabled={disabled}>
<IconCloudUpload className="h-4 w-4 text-muted-foreground" />
<span className="flex-1">Push</span>
<span className="flex-1">{t("integrations:push")}</span>
{hasMatchingUpstream && aheadCount > 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
↑{aheadCount}
@@ -235,7 +270,7 @@ function VcsDropdownItems({
disabled={disabled}
>
<IconCloudUpload className="h-4 w-4 text-muted-foreground" />
<span>Push</span>
<span>{t("integrations:push")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer gap-3"
@@ -243,23 +278,25 @@ function VcsDropdownItems({
disabled={disabled}
>
<IconAlertTriangle className="h-4 w-4 text-muted-foreground" />
<span>Force Push</span>
<span>{t("integrations:forcePush")}</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onRebase} disabled={disabled}>
<IconGitCherryPick className="h-4 w-4 text-muted-foreground" />
<span className="flex-1">Rebase</span>
<span className="flex-1">{t("integrations:rebase")}</span>
{/* The branch is a git ref — data. One message with the ref
interpolated, not an "onto" stem concatenated onto it. */}
<span className="text-xs text-muted-foreground">
onto {baseBranch || DEFAULT_BASE_BRANCH}
{t("integrations:ontoBranch", { branch: baseBranch || DEFAULT_BASE_BRANCH })}
</span>
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onMerge} disabled={disabled}>
<IconGitMerge className="h-4 w-4 text-muted-foreground" />
<span className="flex-1">Merge</span>
<span className="flex-1">{t("integrations:merge")}</span>
<span className="text-xs text-muted-foreground">
from {baseBranch || DEFAULT_BASE_BRANCH}
{t("integrations:fromBranch", { branch: baseBranch || DEFAULT_BASE_BRANCH })}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -273,6 +310,11 @@ type VcsSplitButtonProps = {
className?: string;
};
// The `label` values below stay English on purpose. They are operation *names*
// consumed by `useGitWithFeedback`, which concatenates them into its own English
// toast titles (`${operationName} failed`). `hooks/use-git-with-feedback.ts` is
// not migrated yet, so translating only this half would ship mixed-language
// toasts. They move when that hook's messages do.
function useGitActions(git: ReturnType<typeof useSessionGit>, baseBranch?: string) {
const gitWithFeedback = useGitWithFeedback();
@@ -320,6 +362,7 @@ const VcsSplitButton = memo(function VcsSplitButton({
buttonSize = "sm",
className,
}: VcsSplitButtonProps) {
const { t } = useTranslation();
const git = useSessionGit(sessionId);
const { openCommitDialog, openPRDialog } = useVcsDialogs();
const activePR = useActiveTaskPR();
@@ -347,6 +390,7 @@ const VcsSplitButton = memo(function VcsSplitButton({
hasOpenPR,
);
const primaryButtonConfig = buildPrimaryButtonConfig({
t,
primaryAction,
uncommittedFileCount,
aheadCount,
@@ -439,6 +483,7 @@ function SingleRepoVcsButton({
buttonSize?: ComponentProps<typeof Button>["size"];
showDivergencePills?: boolean;
}) {
const { t } = useTranslation();
return (
<div className={cn("inline-flex", className)}>
<Tooltip>
@@ -469,7 +514,7 @@ function SingleRepoVcsButton({
size={buttonSize}
variant="outline"
className="-ml-px rounded-l-none px-2 cursor-pointer"
aria-label="Open VCS options"
aria-label={t("integrations:openVcsOptions")}
disabled={isDisabled}
>
{isGitLoading ? (
@@ -14,6 +14,7 @@ import {
resolveBaseBranch,
resolveRepositoryId,
} from "@/lib/watcher-repository-default";
import { useTranslation } from "react-i18next";
type PickItem = { id: string; label: string };
@@ -74,6 +75,7 @@ export function WatcherRepositoryFields({
onRepositoryChange: (repositoryId: string) => void;
onBaseBranchChange: (baseBranch: string) => void;
}) {
const { t } = useTranslation();
// forceRefresh: a repo created in settings doesn't update the shared store
// slice, and the lazy fetch is gated by isLoaded — so without this the picker
// could miss repositories created since the slice first loaded. The hook owns
@@ -89,8 +91,8 @@ export function WatcherRepositoryFields({
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<PickSelect
label="Repository"
description="Optional — the repository the agent works in."
label={t("common:repository")}
description={t("common:optionalTheRepositoryTheAgentWorks")}
value={repositoryId || NO_REPOSITORY}
onChange={(v) => onRepositoryChange(resolveRepositoryId(v))}
placeholder={NO_REPOSITORY_LABEL}
@@ -100,8 +102,8 @@ export function WatcherRepositoryFields({
]}
/>
<PickSelect
label="Base Branch"
description="The base branch the agent starts from."
label={t("common:baseBranch")}
description={t("common:theBaseBranchTheAgentStarts")}
value={baseBranch || DEFAULT_BRANCH}
onChange={(v) => onBaseBranchChange(resolveBaseBranch(v))}
placeholder={branchPlaceholder(repositoryId, branchesLoading)}
+10 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { Fragment, memo, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconCheck, IconChevronDown, IconLogicBuffer } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
@@ -25,6 +26,7 @@ function InlineSteps({
steps: StepItem[];
agentProfiles: AgentProfileOption[];
}) {
const { t } = useTranslation();
if (steps.length === 0) return null;
return (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground whitespace-nowrap">
@@ -47,7 +49,7 @@ function InlineSteps({
<TooltipTrigger asChild>
<span className="text-[10px] text-muted-foreground/60 leading-none">*</span>
</TooltipTrigger>
<TooltipContent>Start step</TooltipContent>
<TooltipContent>{t("workflows:startStep")}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
@@ -97,6 +99,7 @@ export const WorkflowSelectorRow = memo(function WorkflowSelectorRow({
lastUsedWorkflowId,
agentProfiles,
}: WorkflowSelectorRowProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const selectedWorkflow = useMemo(
@@ -124,12 +127,16 @@ export const WorkflowSelectorRow = memo(function WorkflowSelectorRow({
data-testid="workflow-selector-trigger"
>
<IconLogicBuffer className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{selectedWorkflow?.name ?? "Select workflow"}</span>
<span className="truncate">
{selectedWorkflow?.name ?? t("workflows:selectWorkflow")}
</span>
<IconChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto min-w-[300px] max-w-none p-1" align="start">
<div className="text-muted-foreground px-2 py-1.5 text-xs border-b">Workflow</div>
<div className="text-muted-foreground px-2 py-1.5 text-xs border-b">
{t("workflows:workflow")}
</div>
{sortedWorkflows.map((wf) => {
const isSelected = wf.id === selectedWorkflowId;
const snapshot = snapshots[wf.id];
@@ -7,6 +7,7 @@ import { useRepoDisplayName } from "@/hooks/domains/session/use-repo-display-nam
import type { WorkspaceContentSearchError } from "@/hooks/domains/session/use-workspace-content-search";
import { groupByRepositoryName, isSingleRepoGroup } from "@/lib/group-by-repo";
import type { ContentSearchMatchRange, WorkspaceContentSearchResult } from "@/lib/types/backend";
import { useTranslation } from "react-i18next";
type PreviewPart = { text: string; matched: boolean };
@@ -137,33 +138,38 @@ export function WorkspaceContentSearch({
sessionId,
onSelect,
}: WorkspaceContentSearchProps) {
const { t } = useTranslation();
const getRepoDisplayName = useRepoDisplayName(sessionId);
if (error === "session-unavailable") {
return <CommandEmpty>Content search needs an active task session.</CommandEmpty>;
return <CommandEmpty>{t("common:contentSearchNeedsAnActiveTask")}</CommandEmpty>;
}
if (error === "query-too-long") {
return <CommandEmpty>Search queries are limited to 200 characters.</CommandEmpty>;
return <CommandEmpty>{t("common:searchQueriesAreLimitedTo200")}</CommandEmpty>;
}
if (error === "transport-error") {
return <CommandEmpty>Search failed. Edit the query or reopen search to retry.</CommandEmpty>;
return <CommandEmpty>{t("common:searchFailedEditTheQueryOr")}</CommandEmpty>;
}
if (isSearching && results.length === 0) {
return (
<CommandEmpty>
<IconLoader2 className="mr-2 inline size-3.5 animate-spin text-muted-foreground" />
Searching task workspace…
{t("common:searchingTaskWorkspace")}
</CommandEmpty>
);
}
if (!search.trim()) return <CommandEmpty>Type to search task contents…</CommandEmpty>;
if (results.length === 0) return <CommandEmpty>No content matches found.</CommandEmpty>;
if (!search.trim()) return <CommandEmpty>{t("common:typeToSearchTaskContents")}</CommandEmpty>;
if (results.length === 0) return <CommandEmpty>{t("common:noContentMatchesFound")}</CommandEmpty>;
const groups = groupByRepositoryName(results, (result) => result.repository_name);
const singleRepo = isSingleRepoGroup(groups);
return groups.map((group) => (
<CommandGroup
key={group.repositoryName}
heading={singleRepo ? "Results" : (getRepoDisplayName(group.repositoryName) ?? "Workspace")}
heading={
singleRepo
? t("common:results")
: (getRepoDisplayName(group.repositoryName) ?? t("common:workspace"))
}
forceMount
data-testid="content-search-repo-group"
data-repository={group.repositoryName}
+34
View File
@@ -1677,4 +1677,38 @@ export const i18nGuardFiles = [
"components/task/share/**/*.{ts,tsx}",
"components/task/inspector/**/*.{ts,tsx}",
"components/task/document/**/*.{ts,tsx}",
// Loose components directly under `components/`. Individual file entries
// rather than a `components/*.tsx` glob: the sibling `components/task-*.tsx`
// files match the `components/(task\/|task-)` namespace rule and belong with
// the `components/task` batches, so a glob here would falsely claim them.
//
// These have no namespace rule of their own, so they are on `common` — except
// the two `vcs-*` menus and `workflow-selector-row`, which were placed on
// `integrations` / `workflows` by hand to sit with the copy they share.
//
// Deliberately left in English inside these files, because they are protocol
// or data rather than copy: the `gh` CLI binary name and the `kdlbs/kandev`
// repository slug in the Improve Kandev dialogs, the `esc` key name in the
// onboarding preview and the command-panel footer, the `make install`/`/commit`
// shell and slash commands, git refs reaching `ontoBranch`/`fromBranch`, and
// the `ActionDef.key` / `RUNTIMES[].id` discriminants that keep React keys
// locale-independent.
"components/branch-refresh-button.tsx",
"components/cli-mode-icon.tsx",
"components/command-panel-footer.tsx",
"components/command-panel-scope-switcher.tsx",
"components/create-local-repository-surface.tsx",
"components/discard-local-changes-dialog.tsx",
"components/enhance-prompt-button.tsx",
"components/folder-picker.tsx",
"components/grid-spinner.tsx",
"components/improve-kandev-dialog-create.tsx",
"components/improve-kandev-dialog.tsx",
"components/onboarding-dialog.tsx",
"components/prompt-result-recovery.tsx",
"components/vcs-multi-repo-menu.tsx",
"components/vcs-split-button.tsx",
"components/watcher-repository-fields.tsx",
"components/workflow-selector-row.tsx",
"components/workspace-content-search.tsx",
];
+161 -6
View File
@@ -2,19 +2,39 @@
"aboutContextCompactionCount": "About inferred compaction count",
"activeSubagents_one": "{{count}} subagent running",
"activeSubagents_other": "{{count}} subagents running",
"activeTasks": "Active Tasks",
"aDirectLocalExecutorProfileIsRequired": "A direct local executor profile is required to create and use an empty repository.",
"agentDashboard": "agent dashboard",
"agentRun": "agent run",
"agentRuns": "agent runs",
"agents": "Agents",
"agentSkillsAskTheAgentTo": "Agent skills (ask the agent to run)",
"allowIndexing": "Allow indexing",
"andMoreFiles_one": "+{{count}} more",
"andMoreFiles_other": "+{{count}} more",
"anEnhancedPromptIsAvailable": "An enhanced prompt is available.",
"apply": "Apply",
"autoApproveAllPermissions": "Auto-approve all permissions",
"automations": "Automations",
"availableTemplates": "Available templates",
"back": "Back",
"backToTaskOverview": "Back to task overview",
"baseBranch": "Base Branch",
"bootsASecondaryKandevDevInstance": "Boots a secondary kandev dev instance with a clean DB so you can verify the change without touching your main one.",
"bootstrapFailed": "Bootstrap failed",
"bootstrapFailedCloseAndReopen": "Bootstrap failed — close and reopen to retry.",
"breadcrumb": "breadcrumb",
"browseParentDirectory": "Browse parent directory",
"bugFix": "Bug fix",
"cancel": "Cancel",
"cancelNewFolder": "Cancel new folder",
"changelog": "Changelog",
"checkingGithubAuthentication": "Checking GitHub authentication…",
"chooseAFolderOrEnterA": "Choose a folder or enter a new path on this machine.",
"cliClient": "CLI client",
"cliModeYourPromptWillBe": "CLI mode — your prompt will be auto-injected into the terminal",
"cliPassthrough": "CLI passthrough",
"close": "Close",
"commandAgentsSettingsKeywords": "agents, agent settings, agent profiles, installed agents, claude",
"commandConfigChatKeywords": "config chat, config mode, configure kandev, workflow configuration, mcp configuration",
"commandExecutorsSettingsKeywords": "executors, executor profiles, execution environment, environment variables, runtime, compute",
@@ -33,6 +53,12 @@
"commandGroupNavigation": "Navigation",
"commandGroupPreferences": "Preferences",
"commandGroupSettings": "Settings",
"commandPaletteMode": "Command palette mode",
"commandPreviewAddTerminalPanel": "Add Terminal Panel",
"commandPreviewCommitChanges": "Commit Changes",
"commandPreviewGoToHome": "Go to Home",
"commandPreviewPull": "Pull",
"commandPreviewSearchTasks": "Search Tasks",
"commandPromptsSettingsKeywords": "prompts, prompt settings, custom prompts, prompt snippets, prompt templates",
"commandQuickChat": "Quick Chat",
"commandQuickChatKeywords": "quick chat, new quick chat, quick question, ask agent",
@@ -40,44 +66,118 @@
"commandSwitchToLightMode": "Switch to Light Mode",
"commandThemeKeywords": "theme, color theme, appearance",
"commandWorkspaceSettingsKeywords": "workspace, workspaces",
"commitsAndPushesToTheCurrent": "Commits and pushes to the current branch.",
"commitsPushesAndOpensAPull": "Commits, pushes, and opens a pull request.",
"configurationChat": "Configuration Chat",
"configureAUtilityAgentToEnhance": "Configure a utility agent in settings to enable AI enhancement",
"configureExecutorsInSettingsToControl": "Configure executors in Settings to control where agents execute.",
"configureGithub": "Configure GitHub",
"confirm": "Confirm",
"contents": "Contents",
"contentSearchNeedsAnActiveTask": "Content search needs an active task session.",
"contextCompactionCountHelp": "Kandev infers this count from observed context usage drops. Missing samples or provider resets can make it approximate.",
"contextCompactions": "Compactions",
"contribute": "Contribute",
"contributingAsLogin": "Contributing as <1>@{{login}}</1>. {{access}}",
"contributingAsYourGithubAccount": "Contributing as your GitHub account. {{access}}",
"copy": "Copy",
"couldNotPrepareImproveKandev": "Could not prepare Improve Kandev",
"createATaskDescribingYourBug": "Create a task describing your bug or feature request",
"createAWorkspaceToConfigureThisIntegration": "Create a workspace to configure this integration.",
"createFolder": "Create folder",
"createNewRepository": "Create new repository",
"createRepository": "Create repository",
"createsAnEmptyGitRepository": "Creates an empty Git repository and registers it in this workspace.",
"creating": "Creating…",
"dAgo": "{{diffDay}}d ago",
"default": "default",
"defaultBadge": "Default",
"deleteSession": "Delete session",
"describeABugYouHitOr": "Describe a bug you hit or a feature you'd like, and we'll create a task on your own agent to implement it in the kandev codebase.",
"destination": "Destination",
"discardAndContinue": "Discard and continue",
"discardLocalChanges": "Discard local changes?",
"discardLocalChangesAtPathDescription": "Starting this task will permanently discard the uncommitted changes in your local clone at {{repoPath}}. Back up anything you want to keep before continuing.",
"discardLocalChangesDescription": "Starting this task will permanently discard the uncommitted changes in your local clone. Back up anything you want to keep before continuing.",
"doNotShowThisAgain": "Do not show this again",
"edit": "Edit",
"editTask": "Edit task",
"gitScopeSkipped": "Skipped {{scope}} because a child repository operation failed ({{failedScopes}}).",
"emptyRepositoriesRunDirectlySwitch": "Empty repositories run directly on this machine. This task will switch to “{{profile}}”.",
"enhancePromptWithAi": "Enhance prompt with AI",
"enterAValue": "Enter a value...",
"enterValue": "Enter value...",
"executors": "Executors",
"externalMcp": "External MCP",
"failedToCreateFolder": "Failed to create folder",
"failedToCreateRepository": "Failed to create repository",
"failedToLoadDirectory": "Failed to load directory",
"failedToLoadRoutine": "Failed to load routine",
"failedToLoadSetup": "Failed to load setup",
"featureRequest": "Feature request",
"files": "Files",
"folderName": "Folder name",
"folders": "Folders",
"forwardStdinStdoutStraightToThe": "Forward stdin/stdout straight to the CLI subprocess. Disables ACP.",
"getStarted": "Get Started",
"githubCliNotAuthenticated": "GitHub CLI not authenticated",
"gitScopeSkipped": "Skipped {{scope}} because a child repository operation failed ({{failedScopes}}).",
"hAgo": "{{diffHr}}h ago",
"hideAdvancedOptions": "Hide advanced options",
"howLogContextWorks": "How log context works",
"improveKandev": "Improve Kandev",
"improveKandevBugPlaceholder": "E.g. When I open the kanban board, the column header text overlaps the task count badge on screens narrower than 1200px...",
"improveKandevFeaturePlaceholder": "E.g. Add a keyboard shortcut to mark a task as Done from the task detail view, and show the shortcut in the task action menu...",
"improveKandevIssuePlaceholder": "Describe the problem or request you want published. The agent will ask for any details required by the matching GitHub issue template...",
"includeRecentBackendAndBrowserLogs": "Include recent backend & browser logs as context for the agent",
"integrations": "Integrations",
"justNow": "just now",
"kandevAllowsEveryAgentPermissionRequest": "Kandev allows every agent permission request without prompting you.",
"kandevCouldNotLoad": "Kandev couldn’t load.",
"kandevIsOpenSourceAndYou": "Kandev is open source, and you can help make it better.",
"kandevKeepsASmallInMemory": "Kandev keeps a small in-memory ring buffer of the most recent backend logs and browser console events. When enabled, those logs are written to a temporary folder on your machine, and the file paths are appended to the task description so the agent can read them while investigating.",
"lastRefreshFailed": "Last refresh failed: {{error}}",
"loading": "Loading…",
"loadingEllipsis": "Loading...",
"loadingFolder": "Loading folder…",
"loadingIndicatorLabel": "Loading",
"loadingLabel": "Loading {{label}}...",
"loadingRoute": "Loading {{routeName}}…",
"loadingRoutine": "Loading routine...",
"loadingTask": "Loading task…",
"loadingWorkflowTemplates": "Loading workflow templates...",
"mAgo": "{{diffMin}}m ago",
"mode": "Mode",
"model": "Model",
"multiRepoUnavailableLocal": "Multi-repo tasks are unavailable on Local until its initial launch path can project sibling repositories.",
"multiRepoUnavailableRemoteDocker": "Multi-repo tasks are unavailable on Remote Docker until it supports creating task instances.",
"multiRepoUnsupportedExecutor": "Multi-repo tasks are not supported by this executor.",
"navigate": "Navigate",
"navigateBetweenPagesSearchTasksTrigger": "Navigate between pages, search tasks, trigger git operations, and manage panels — all without leaving the keyboard. Context-aware commands appear based on the active page.",
"newFolder": "New folder",
"newFolderName": "New folder name",
"next": "Next",
"noCliClientsInstalledYetInstall": "No CLI clients installed yet. Install Claude / Codex / OpenCode / Amp to enable this picker.",
"noCommandsFound": "No commands found.",
"noContentMatchesFound": "No content matches found.",
"noFilesFound": "No files found.",
"noFoldersHerePickThisOne": "No folders here — pick this one or go up",
"officeIsNotEnabledForThis": "Office is not enabled for this runtime.",
"officeRouteNotPortedYet": "This Office route is handled by the SPA shell, but its dedicated client page is still being ported: <1>{{pathname}}</1>",
"onboardingStepAgentsDescription": "Manage discovered agents and install new ones.",
"onboardingStepAgentsTitle": "AI Agents",
"onboardingStepCommandPanelDescription": "Quick access to actions from anywhere with a keyboard shortcut.",
"onboardingStepCommandPanelTitle": "Command Panel",
"onboardingStepExecutorsDescription": "Agents can run in different executor environments — local, containerized, or remote.",
"onboardingStepExecutorsTitle": "Executors",
"onboardingStepWorkflowsDescription": "Workflows define the steps and automation for your tasks.",
"onboardingStepWorkflowsTitle": "Agentic Workflows",
"open": "Open",
"openFullPage": "Open full page",
"openingLastTask": "Opening last task…",
"openIssue": "Open issue",
"optionalTheRepositoryTheAgentWorks": "Optional — the repository the agent works in.",
"panels": "Panels",
"parentDirectory": "Parent directory",
"permitTheCliToUploadCode": "Permit the CLI to upload code for cloud indexing (auggie / similar).",
"pickACliClient": "Pick a CLI client",
"pickACliClientToLoad": "Pick a CLI client to load its available models and modes.",
@@ -85,6 +185,8 @@
"pluginPanelUnavailable": "This panel is no longer available.",
"plugins": "Plugins",
"prDetails": "PR Details",
"preparingKandevRepository": "Preparing kandev repository…",
"preparingKandevRepositoryInBackground": "Preparing kandev repository in background. Fill in the details, submit when ready.",
"presetIconBug": "Bug",
"presetIconCheck": "Check",
"presetIconCode": "Code",
@@ -93,15 +195,22 @@
"presetIconSearch": "Search",
"presetIconSparkle": "Sparkle",
"presetIconTool": "Tool",
"pressCmdKToOpenItAnytime": "<0>Press</0><1></1><2>to open it anytime</2>",
"pressEnterToConfirm": "Press Enter to confirm",
"profileName": "Profile name",
"prompts": "Prompts",
"refreshAriaLabel": "Refresh {{label}}",
"refreshBranchesGitFetch": "Refresh branches (git fetch)",
"refreshingLabel": "Refreshing {{label}}...",
"refreshLabelLastFetched": "Refresh {{label}} (last fetched {{time}})",
"reload": "Reload",
"reloadTheApplicationToTryAgain": "Reload the application to try again.",
"requestFailed": "Request failed",
"rename": "Rename",
"reviewSubmoduleBoundary": "Submodule: {{scope}}",
"repository": "Repository",
"repositoryName": "Repository name",
"requestFailed": "Request failed",
"reset": "Reset",
"resizeTableColumns": "Resize table columns {{left}} and {{right}}",
"resetting": "Resetting…",
"resetWatchChecking": "Checking how many tasks would be deleted…",
"resetWatchDeleteAllTasks": "This will delete every task previously created by the watch, including archived tasks. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
"resetWatchDeleteTasks_one": "This will delete {{count}} task previously created by the watch, including archived tasks. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
@@ -109,24 +218,70 @@
"resetWatchNoTasks": "No tasks were created by this watch yet — only the polling state will be cleared. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
"resetWatchPreviewFailedRequired": "Could not load the affected task count. Retry the preview before resetting this watch.",
"resetWatchTitle": "Reset {{label}}?",
"resetting": "Resetting…",
"resizeTableColumns": "Resize table columns {{left}} and {{right}}",
"results": "Results",
"retryPreview": "Retry preview",
"reviewSubmoduleBoundary": "Submodule: {{scope}}",
"runsFormatTypecheckTestAndLint": "Runs format, typecheck, test, and lint across the monorepo.",
"runtimeDocker": "Docker",
"runtimeDockerDescription": "Containerized execution for full isolation and reproducibility.",
"runtimeGitWorktree": "Git Worktree",
"runtimeGitWorktreeDescription": "Isolated branch environment under a worktree root for parallel work.",
"runtimeLocal": "Local",
"runtimeLocalDescription": "Run agents directly on your machine with full access to your local filesystem.",
"runtimeSprites": "Sprites (Remote sprites.dev)",
"runtimeSpritesDescription": "Hardware-isolated execution environment for arbitrary code.",
"save": "Save",
"scopeCommands": "Commands",
"scopeContents": "Contents",
"scopeFiles": "Files",
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
"scratchWorkspace": "scratch workspace",
"searchFailedEditTheQueryOr": "Search failed. Edit the query or reopen search to retry.",
"searchForFiles": "Search for files...",
"searchForTasks": "Search for tasks...",
"searchingTaskWorkspace": "Searching task workspace…",
"searchQueriesAreLimitedTo200": "Search queries are limited to 200 characters.",
"searchTaskContents": "Search task contents…",
"select": "Select",
"settings": "Settings",
"shell": "Shell",
"shellCommandPlusSlashCommandSkills": "Shell command you can run in the secondary instance, plus slash-command <2>skills</2> you can ask the agent to run during the workflow.",
"showAdvancedOptions": "Show advanced options",
"skip": "Skip",
"stagesAndCommitsChangesUsingConventional": "Stages and commits changes using Conventional Commits.",
"status": "Status",
"subtaskTitle": "Subtask title",
"switchMode": "Switch mode",
"system": "System",
"taskLoading": "Loading task...",
"tasks": "Tasks",
"taskUnavailable": "Task unavailable",
"taskUnavailableDescription": "We could not load this task. It may have been deleted or you may not have access.",
"theAgentForksKandevToYourAccount": "The agent forks <1>{{repo}}</1> to your GitHub account and opens a PR from your fork, credited to you",
"theBaseBranchTheAgentStarts": "The base branch the agent starts from.",
"theFinalStepOpensAPullRequest": "The final step of this workflow opens a pull request, which needs the <1>{{binary}}</1> CLI to be authenticated. {{message}}",
"thisEmptyRepositoryWillRunWith": "This empty repository will run with “{{profile}}” on this machine.",
"thisGithubAccountCannotForkKdlbs": "This GitHub account cannot fork kdlbs/kandev.",
"thisPageCouldNotLoad": "This page couldn’t load.",
"thisWorkflowWillOnlyCreateA": "This workflow will only create a GitHub issue from the repository template. The agent will ask for missing details and publication approval; it will not implement the change or open a pull request.",
"title": "Title",
"toastRegionLabel": "Notifications",
"typeACommand": "Type a command...",
"typeToSearchFiles": "Type to search files...",
"typeToSearchTaskContents": "Type to search task contents…",
"unknownPanel": "Unknown panel: {{component}}",
"unsavedChanges": "Unsaved changes",
"usefulCommandsAgentSkills": "Useful commands & agent skills",
"useScratchInstead": "Use scratch instead",
"useThisFolder": "Use this folder",
"waitsForCiAndAutomatedReviews": "Waits for CI and automated reviews, then fixes failures and addresses comments.",
"whenItsDoneTheAgentOpensAPr": "When it's done, the agent opens a pull request to <2>{{repo}}</2> for the maintainers to review, saving them time and shipping the improvement to everyone.",
"workflow": "Workflow",
"workflowsControlTheStepsAutomationAnd": "Workflows control the steps, automation, and agent behavior for your tasks. You can add more workflows from Settings.",
"workspace": "Workspace",
"workspaces": "Workspaces",
"youCanUseTheNavigationOr": "You can use the navigation or reload to try again."
"youCanUseTheNavigationOr": "You can use the navigation or reload to try again.",
"yourAgentImplementsItInThe": "Your agent implements it in the kandev repository, with tests",
"youVerifyAndTestTheChange": "You verify and test the change in a second kandev instance"
}
+27 -4
View File
@@ -2,31 +2,38 @@
"addDetailsAboutThisChange": "Add details about this change...",
"allRepos": "All repos",
"anErrorOccurred": "An error occurred",
"authRequiredHeading": "{{name}} authentication required",
"authRequiredInline": "{{name}} authentication required.",
"authenticated": "Authenticated",
"authenticationFailed": "Authentication failed: {{error}}",
"authRequiredHeading": "{{name}} authentication required",
"authRequiredInline": "{{name}} authentication required.",
"branchPushedNotCreated": "Branch pushed; {{shortName}} not created",
"branchWasPushedRetryCreation": "Branch was pushed; retry {{longName}} creation.",
"checkedRelative": "· checked {{relative}}",
"commit": "Commit",
"commitChangedFiles_one": "Commit {{count}} changed file",
"commitChangedFiles_other": "Commit {{count}} changed files",
"commitChanges": "Commit Changes",
"commitChangesScoped": "Commit Changes — {{scopedRepo}}",
"commitsAheadAriaLabel": "{{value}} commits ahead",
"commitsBehindAriaLabel": "{{value}} commits behind",
"committingEllipsis": "Committing...",
"configureAUtilityAgentInSettings": "Configure a utility agent in settings to enable AI generation",
"copiedConfigTo": "Copied {{label}} config to {{targetName}}.",
"copyConfig": "Copy config",
"copyConfigSettingsOnly": "Copy the {{label}} settings from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
"copyConfigToAnotherWorkspace": "Copy config to another workspace",
"copyConfigWithCredentials": "Copy the {{label}} settings and credentials from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
"copyConfiguration": "Copy {{label}} configuration",
"copyConfigWithCredentials": "Copy the {{label}} settings and credentials from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
"copyingConfigTo": "Copying {{label}} config to {{targetName}}…",
"createAsDraft": "Create as draft",
"createdSuccessfully": "{{longName}} created successfully",
"createFailed": "Create {{shortName}} failed",
"createLongName": "Create {{longName}}",
"createPr": "Create PR",
"createPrCommitsAhead_one": "Create PR ({{count}} commit ahead)",
"createPrCommitsAhead_other": "Create PR ({{count}} commits ahead)",
"createScoped": "Create {{longName}} — {{scopedRepo}}",
"createShortName": "Create {{shortName}}",
"createdSuccessfully": "{{longName}} created successfully",
"creatingEllipsis": "Creating...",
"creatingFrom": "Creating {{shortName}} from <4>{{displayBranch}}</4>",
"creatingFromInto": "Creating {{shortName}} from <4>{{displayBranch}}</4> → <6>{{baseBranch}}</6>",
@@ -40,6 +47,8 @@
"failedToCopyConfig": "Failed to copy config: {{err}}",
"filesChanged_one": "<0>{{count}}</0> file changed",
"filesChanged_other": "<0>{{count}}</0> files changed",
"forcePush": "Force Push",
"fromBranch": "from {{branch}}",
"generateCommitDescriptionWithAi": "Generate commit description with AI",
"generateCommitMessageWithAi": "Generate commit message with AI",
"generateDescriptionWithAi": "Generate {{shortName}} description with AI",
@@ -47,8 +56,22 @@
"loading": "Loading…",
"longNameTitle": "{{longName}} title",
"longNameTitlePlaceholder": "{{longName}} title...",
"merge": "Merge",
"noChangesToCommit": "No changes to commit",
"noSavedQueriesYet": "No saved queries yet",
"ontoBranch": "onto {{branch}}",
"openVcsOptions": "Open VCS options",
"pickActionThenRepo": "Pick action, then repo",
"pickARepositoryForCommit": "Pick a repository for Commit",
"pickARepositoryForCreatePr": "Pick a repository for Create PR",
"pickARepositoryForPush": "Pick a repository for Push",
"pickARepositoryForRebase": "Pick a repository for Rebase",
"pull": "Pull",
"push": "Push",
"pushCommitsToRemote_one": "Push {{count}} commit to remote",
"pushCommitsToRemote_other": "Push {{count}} commits to remote",
"rebase": "Rebase",
"rebaseOntoBranchBehind": "Rebase onto {{branch}} ({{behind}} behind)",
"reconnectProvider": "Reconnect {{name}}",
"repository": "Repository",
"retryShortName": "Retry {{shortName}}",
+1
View File
@@ -225,6 +225,7 @@
"wipLimit": "WIP limit",
"wipLimitHelp": "Maximum admitted (active) tasks in this step. Overflow stays visible in a queue. Use 0 for unlimited.",
"wipLimitsNote": "WIP limits active work, not visibility. Overflow remains on the board until capacity opens; if a selected feeder is also full, new task creation is rejected.",
"workflow": "Workflow",
"workflowCycleBlocked": "Workflow cycle blocked",
"workflowCycleBlockedDescription": "Change the workflow steps to remove the automatic cycle before continuing.",
"workflowCycleBlockingBody": "This path re-enters \"{{stepName}}\" and can start the agent again without another user action.",
+161 -6
View File
@@ -2,19 +2,39 @@
"aboutContextCompactionCount": "Àƀōũţ ĩńƒēŕŕēď ćōḿƥàćţĩōń ćōũńţ",
"activeSubagents_one": "{{count}} śũƀàĝēńţ ŕũńńĩńĝ",
"activeSubagents_other": "{{count}} śũƀàĝēńţś ŕũńńĩńĝ",
"activeTasks": "Àćţĩvē Ţàśķś",
"aDirectLocalExecutorProfileIsRequired": "À ďĩŕēćţ ĺōćàĺ ēxēćũţōŕ ƥŕōƒĩĺē ĩś ŕēqũĩŕēď ţō ćŕēàţē àńď ũśē àń ēḿƥţŷ ŕēƥōśĩţōŕŷ.",
"agentDashboard": "àĝēńţ ďàśĥƀōàŕď",
"agentRun": "àĝēńţ ŕũń",
"agentRuns": "àĝēńţ ŕũńś",
"agents": "Àĝēńţś",
"agentSkillsAskTheAgentTo": "Àĝēńţ śķĩĺĺś (àśķ ţĥē àĝēńţ ţō ŕũń)",
"allowIndexing": "Àĺĺōŵ ĩńďēxĩńĝ",
"andMoreFiles_one": "+{{count}} ḿōŕē",
"andMoreFiles_other": "+{{count}} ḿōŕē",
"anEnhancedPromptIsAvailable": "Àń ēńĥàńćēď ƥŕōḿƥţ ĩś àvàĩĺàƀĺē.",
"apply": "Àƥƥĺŷ",
"autoApproveAllPermissions": "Àũţō-àƥƥŕōvē àĺĺ ƥēŕḿĩśśĩōńś",
"automations": "Àũţōḿàţĩōńś",
"availableTemplates": "Àvàĩĺàƀĺē ţēḿƥĺàţēś",
"back": "Ɓàćķ",
"backToTaskOverview": "Ɓàćķ ţō ţàśķ ōvēŕvĩēŵ",
"baseBranch": "Ɓàśē Ɓŕàńćĥ",
"bootsASecondaryKandevDevInstance": "Ɓōōţś à śēćōńďàŕŷ ķàńďēv ďēv ĩńśţàńćē ŵĩţĥ à ćĺēàń ĎƁ śō ŷōũ ćàń vēŕĩƒŷ ţĥē ćĥàńĝē ŵĩţĥōũţ ţōũćĥĩńĝ ŷōũŕ ḿàĩń ōńē.",
"bootstrapFailed": "Ɓōōţśţŕàƥ ƒàĩĺēď",
"bootstrapFailedCloseAndReopen": "Ɓōōţśţŕàƥ ƒàĩĺēď — ćĺōśē àńď ŕēōƥēń ţō ŕēţŕŷ.",
"breadcrumb": "ƀŕēàďćŕũḿƀ",
"browseParentDirectory": "Ɓŕōŵśē ƥàŕēńţ ďĩŕēćţōŕŷ",
"bugFix": "Ɓũĝ ƒĩx",
"cancel": "Ćàńćēĺ",
"cancelNewFolder": "Ćàńćēĺ ńēŵ ƒōĺďēŕ",
"changelog": "Ćĥàńĝēĺōĝ",
"checkingGithubAuthentication": "Ćĥēćķĩńĝ ĜĩţĤũƀ àũţĥēńţĩćàţĩōń…",
"chooseAFolderOrEnterA": "Ćĥōōśē à ƒōĺďēŕ ōŕ ēńţēŕ à ńēŵ ƥàţĥ ōń ţĥĩś ḿàćĥĩńē.",
"cliClient": "ĆĹĨ ćĺĩēńţ",
"cliModeYourPromptWillBe": "ĆĹĨ ḿōďē — ŷōũŕ ƥŕōḿƥţ ŵĩĺĺ ƀē àũţō-ĩńĵēćţēď ĩńţō ţĥē ţēŕḿĩńàĺ",
"cliPassthrough": "ĆĹĨ ƥàśśţĥŕōũĝĥ",
"close": "Ćĺōśē",
"commandAgentsSettingsKeywords": "àĝēńţś, àĝēńţ śēţţĩńĝś, àĝēńţ ƥŕōƒĩĺēś, ĩńśţàĺĺēď àĝēńţś, ćĺàũďē",
"commandConfigChatKeywords": "ćōńƒĩĝ ćĥàţ, ćōńƒĩĝ ḿōďē, ćōńƒĩĝũŕē ķàńďēv, ŵōŕķƒĺōŵ ćōńƒĩĝũŕàţĩōń, ḿćƥ ćōńƒĩĝũŕàţĩōń",
"commandExecutorsSettingsKeywords": "ēxēćũţōŕś, ēxēćũţōŕ ƥŕōƒĩĺēś, ēxēćũţĩōń ēńvĩŕōńḿēńţ, ēńvĩŕōńḿēńţ vàŕĩàƀĺēś, ŕũńţĩḿē, ćōḿƥũţē",
@@ -33,6 +53,12 @@
"commandGroupNavigation": "Ńàvĩĝàţĩōń",
"commandGroupPreferences": "Ƥŕēƒēŕēńćēś",
"commandGroupSettings": "Śēţţĩńĝś",
"commandPaletteMode": "Ćōḿḿàńď ƥàĺēţţē ḿōďē",
"commandPreviewAddTerminalPanel": "Àďď Ţēŕḿĩńàĺ Ƥàńēĺ",
"commandPreviewCommitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
"commandPreviewGoToHome": "Ĝō ţō Ĥōḿē",
"commandPreviewPull": "Ƥũĺĺ",
"commandPreviewSearchTasks": "Śēàŕćĥ Ţàśķś",
"commandPromptsSettingsKeywords": "ƥŕōḿƥţś, ƥŕōḿƥţ śēţţĩńĝś, ćũśţōḿ ƥŕōḿƥţś, ƥŕōḿƥţ śńĩƥƥēţś, ƥŕōḿƥţ ţēḿƥĺàţēś",
"commandQuickChat": "Qũĩćķ Ćĥàţ",
"commandQuickChatKeywords": "qũĩćķ ćĥàţ, ńēŵ qũĩćķ ćĥàţ, qũĩćķ qũēśţĩōń, àśķ àĝēńţ",
@@ -40,44 +66,118 @@
"commandSwitchToLightMode": "Śŵĩţćĥ ţō Ĺĩĝĥţ Ḿōďē",
"commandThemeKeywords": "ţĥēḿē, ćōĺōŕ ţĥēḿē, àƥƥēàŕàńćē",
"commandWorkspaceSettingsKeywords": "ŵōŕķśƥàćē, ŵōŕķśƥàćēś",
"commitsAndPushesToTheCurrent": "Ćōḿḿĩţś àńď ƥũśĥēś ţō ţĥē ćũŕŕēńţ ƀŕàńćĥ.",
"commitsPushesAndOpensAPull": "Ćōḿḿĩţś, ƥũśĥēś, àńď ōƥēńś à ƥũĺĺ ŕēqũēśţ.",
"configurationChat": "Ćōńƒĩĝũŕàţĩōń Ćĥàţ",
"configureAUtilityAgentToEnhance": "Ćōńƒĩĝũŕē à ũţĩĺĩţŷ àĝēńţ ĩń śēţţĩńĝś ţō ēńàƀĺē ÀĨ ēńĥàńćēḿēńţ",
"configureExecutorsInSettingsToControl": "Ćōńƒĩĝũŕē ēxēćũţōŕś ĩń Śēţţĩńĝś ţō ćōńţŕōĺ ŵĥēŕē àĝēńţś ēxēćũţē.",
"configureGithub": "Ćōńƒĩĝũŕē ĜĩţĤũƀ",
"confirm": "Ćōńƒĩŕḿ",
"contents": "Ćōńţēńţś",
"contentSearchNeedsAnActiveTask": "Ćōńţēńţ śēàŕćĥ ńēēďś àń àćţĩvē ţàśķ śēśśĩōń.",
"contextCompactionCountHelp": "Ķàńďēv ĩńƒēŕś ţĥĩś ćōũńţ ƒŕōḿ ōƀśēŕvēď ćōńţēxţ ũśàĝē ďŕōƥś. Ḿĩśśĩńĝ śàḿƥĺēś ōŕ ƥŕōvĩďēŕ ŕēśēţś ćàń ḿàķē ĩţ àƥƥŕōxĩḿàţē.",
"contextCompactions": "Ćōḿƥàćţĩōńś",
"contribute": "Ćōńţŕĩƀũţē",
"contributingAsLogin": "Ćōńţŕĩƀũţĩńĝ àś <1>@{{login}}</1>. {{access}}",
"contributingAsYourGithubAccount": "Ćōńţŕĩƀũţĩńĝ àś ŷōũŕ ĜĩţĤũƀ àććōũńţ. {{access}}",
"copy": "Ćōƥŷ",
"couldNotPrepareImproveKandev": "Ćōũĺď ńōţ ƥŕēƥàŕē Ĩḿƥŕōvē Ķàńďēv",
"createATaskDescribingYourBug": "Ćŕēàţē à ţàśķ ďēśćŕĩƀĩńĝ ŷōũŕ ƀũĝ ōŕ ƒēàţũŕē ŕēqũēśţ",
"createAWorkspaceToConfigureThisIntegration": "Ćŕēàţē à ŵōŕķśƥàćē ţō ćōńƒĩĝũŕē ţĥĩś ĩńţēĝŕàţĩōń.",
"createFolder": "Ćŕēàţē ƒōĺďēŕ",
"createNewRepository": "Ćŕēàţē ńēŵ ŕēƥōśĩţōŕŷ",
"createRepository": "Ćŕēàţē ŕēƥōśĩţōŕŷ",
"createsAnEmptyGitRepository": "Ćŕēàţēś àń ēḿƥţŷ Ĝĩţ ŕēƥōśĩţōŕŷ àńď ŕēĝĩśţēŕś ĩţ ĩń ţĥĩś ŵōŕķśƥàćē.",
"creating": "Ćŕēàţĩńĝ…",
"dAgo": "{{diffDay}}ď àĝō",
"default": "ďēƒàũĺţ",
"defaultBadge": "Ďēƒàũĺţ",
"deleteSession": "Ďēĺēţē śēśśĩōń",
"describeABugYouHitOr": "Ďēśćŕĩƀē à ƀũĝ ŷōũ ĥĩţ ōŕ à ƒēàţũŕē ŷōũ'ď ĺĩķē, àńď ŵē'ĺĺ ćŕēàţē à ţàśķ ōń ŷōũŕ ōŵń àĝēńţ ţō ĩḿƥĺēḿēńţ ĩţ ĩń ţĥē ķàńďēv ćōďēƀàśē.",
"destination": "Ďēśţĩńàţĩōń",
"discardAndContinue": "Ďĩśćàŕď àńď ćōńţĩńũē",
"discardLocalChanges": "Ďĩśćàŕď ĺōćàĺ ćĥàńĝēś?",
"discardLocalChangesAtPathDescription": "Śţàŕţĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďĩśćàŕď ţĥē ũńćōḿḿĩţţēď ćĥàńĝēś ĩń ŷōũŕ ĺōćàĺ ćĺōńē àţ {{repoPath}}. Ɓàćķ ũƥ àńŷţĥĩńĝ ŷōũ ŵàńţ ţō ķēēƥ ƀēƒōŕē ćōńţĩńũĩńĝ.",
"discardLocalChangesDescription": "Śţàŕţĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďĩśćàŕď ţĥē ũńćōḿḿĩţţēď ćĥàńĝēś ĩń ŷōũŕ ĺōćàĺ ćĺōńē. Ɓàćķ ũƥ àńŷţĥĩńĝ ŷōũ ŵàńţ ţō ķēēƥ ƀēƒōŕē ćōńţĩńũĩńĝ.",
"doNotShowThisAgain": "Ďō ńōţ śĥōŵ ţĥĩś àĝàĩń",
"edit": "Ēďĩţ",
"editTask": "Ēďĩţ ţàśķ",
"gitScopeSkipped": "Śķĩƥƥēď {{scope}} ƀēćàũśē à ćĥĩĺď ŕēƥōśĩţōŕŷ ōƥēŕàţĩōń ƒàĩĺēď ({{failedScopes}}).",
"emptyRepositoriesRunDirectlySwitch": "Ēḿƥţŷ ŕēƥōśĩţōŕĩēś ŕũń ďĩŕēćţĺŷ ōń ţĥĩś ḿàćĥĩńē. Ţĥĩś ţàśķ ŵĩĺĺ śŵĩţćĥ ţō “{{profile}}”.",
"enhancePromptWithAi": "Ēńĥàńćē ƥŕōḿƥţ ŵĩţĥ ÀĨ",
"enterAValue": "Ēńţēŕ à vàĺũē...",
"enterValue": "Ēńţēŕ vàĺũē...",
"executors": "Ēxēćũţōŕś",
"externalMcp": "Ēxţēŕńàĺ ḾĆƤ",
"failedToCreateFolder": "Ƒàĩĺēď ţō ćŕēàţē ƒōĺďēŕ",
"failedToCreateRepository": "Ƒàĩĺēď ţō ćŕēàţē ŕēƥōśĩţōŕŷ",
"failedToLoadDirectory": "Ƒàĩĺēď ţō ĺōàď ďĩŕēćţōŕŷ",
"failedToLoadRoutine": "Ƒàĩĺēď ţō ĺōàď ŕōũţĩńē",
"failedToLoadSetup": "Ƒàĩĺēď ţō ĺōàď śēţũƥ",
"featureRequest": "Ƒēàţũŕē ŕēqũēśţ",
"files": "Ƒĩĺēś",
"folderName": "Ƒōĺďēŕ ńàḿē",
"folders": "Ƒōĺďēŕś",
"forwardStdinStdoutStraightToThe": "Ƒōŕŵàŕď śţďĩń/śţďōũţ śţŕàĩĝĥţ ţō ţĥē ĆĹĨ śũƀƥŕōćēśś. Ďĩśàƀĺēś ÀĆƤ.",
"getStarted": "Ĝēţ Śţàŕţēď",
"githubCliNotAuthenticated": "ĜĩţĤũƀ ĆĹĨ ńōţ àũţĥēńţĩćàţēď",
"gitScopeSkipped": "Śķĩƥƥēď {{scope}} ƀēćàũśē à ćĥĩĺď ŕēƥōśĩţōŕŷ ōƥēŕàţĩōń ƒàĩĺēď ({{failedScopes}}).",
"hAgo": "{{diffHr}}ĥ àĝō",
"hideAdvancedOptions": "Ĥĩďē àďvàńćēď ōƥţĩōńś",
"howLogContextWorks": "Ĥōŵ ĺōĝ ćōńţēxţ ŵōŕķś",
"improveKandev": "Ĩḿƥŕōvē Ķàńďēv",
"improveKandevBugPlaceholder": "Ē.ĝ. Ŵĥēń Ĩ ōƥēń ţĥē ķàńƀàń ƀōàŕď, ţĥē ćōĺũḿń ĥēàďēŕ ţēxţ ōvēŕĺàƥś ţĥē ţàśķ ćōũńţ ƀàďĝē ōń śćŕēēńś ńàŕŕōŵēŕ ţĥàń 1200ƥx...",
"improveKandevFeaturePlaceholder": "Ē.ĝ. Àďď à ķēŷƀōàŕď śĥōŕţćũţ ţō ḿàŕķ à ţàśķ àś Ďōńē ƒŕōḿ ţĥē ţàśķ ďēţàĩĺ vĩēŵ, àńď śĥōŵ ţĥē śĥōŕţćũţ ĩń ţĥē ţàśķ àćţĩōń ḿēńũ...",
"improveKandevIssuePlaceholder": "Ďēśćŕĩƀē ţĥē ƥŕōƀĺēḿ ōŕ ŕēqũēśţ ŷōũ ŵàńţ ƥũƀĺĩśĥēď. Ţĥē àĝēńţ ŵĩĺĺ àśķ ƒōŕ àńŷ ďēţàĩĺś ŕēqũĩŕēď ƀŷ ţĥē ḿàţćĥĩńĝ ĜĩţĤũƀ ĩśśũē ţēḿƥĺàţē...",
"includeRecentBackendAndBrowserLogs": "Ĩńćĺũďē ŕēćēńţ ƀàćķēńď & ƀŕōŵśēŕ ĺōĝś àś ćōńţēxţ ƒōŕ ţĥē àĝēńţ",
"integrations": "Ĩńţēĝŕàţĩōńś",
"justNow": "ĵũśţ ńōŵ",
"kandevAllowsEveryAgentPermissionRequest": "Ķàńďēv àĺĺōŵś ēvēŕŷ àĝēńţ ƥēŕḿĩśśĩōń ŕēqũēśţ ŵĩţĥōũţ ƥŕōḿƥţĩńĝ ŷōũ.",
"kandevCouldNotLoad": "Ķàńďēv ćōũĺďń’ţ ĺōàď.",
"kandevIsOpenSourceAndYou": "Ķàńďēv ĩś ōƥēń śōũŕćē, àńď ŷōũ ćàń ĥēĺƥ ḿàķē ĩţ ƀēţţēŕ.",
"kandevKeepsASmallInMemory": "Ķàńďēv ķēēƥś à śḿàĺĺ ĩń-ḿēḿōŕŷ ŕĩńĝ ƀũƒƒēŕ ōƒ ţĥē ḿōśţ ŕēćēńţ ƀàćķēńď ĺōĝś àńď ƀŕōŵśēŕ ćōńśōĺē ēvēńţś. Ŵĥēń ēńàƀĺēď, ţĥōśē ĺōĝś àŕē ŵŕĩţţēń ţō à ţēḿƥōŕàŕŷ ƒōĺďēŕ ōń ŷōũŕ ḿàćĥĩńē, àńď ţĥē ƒĩĺē ƥàţĥś àŕē àƥƥēńďēď ţō ţĥē ţàśķ ďēśćŕĩƥţĩōń śō ţĥē àĝēńţ ćàń ŕēàď ţĥēḿ ŵĥĩĺē ĩńvēśţĩĝàţĩńĝ.",
"lastRefreshFailed": "Ĺàśţ ŕēƒŕēśĥ ƒàĩĺēď: {{error}}",
"loading": "Ĺōàďĩńĝ…",
"loadingEllipsis": "Ĺōàďĩńĝ...",
"loadingFolder": "Ĺōàďĩńĝ ƒōĺďēŕ…",
"loadingIndicatorLabel": "Ĺōàďĩńĝ",
"loadingLabel": "Ĺōàďĩńĝ {{label}}...",
"loadingRoute": "Ĺōàďĩńĝ {{routeName}}…",
"loadingRoutine": "Ĺōàďĩńĝ ŕōũţĩńē...",
"loadingTask": "Ĺōàďĩńĝ ţàśķ…",
"loadingWorkflowTemplates": "Ĺōàďĩńĝ ŵōŕķƒĺōŵ ţēḿƥĺàţēś...",
"mAgo": "{{diffMin}}ḿ àĝō",
"mode": "Ḿōďē",
"model": "Ḿōďēĺ",
"multiRepoUnavailableLocal": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ũńàvàĩĺàƀĺē ōń Ĺōćàĺ ũńţĩĺ ĩţś ĩńĩţĩàĺ ĺàũńćĥ ƥàţĥ ćàń ƥŕōĵēćţ śĩƀĺĩńĝ ŕēƥōśĩţōŕĩēś.",
"multiRepoUnavailableRemoteDocker": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ũńàvàĩĺàƀĺē ōń Ŕēḿōţē Ďōćķēŕ ũńţĩĺ ĩţ śũƥƥōŕţś ćŕēàţĩńĝ ţàśķ ĩńśţàńćēś.",
"multiRepoUnsupportedExecutor": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ńōţ śũƥƥōŕţēď ƀŷ ţĥĩś ēxēćũţōŕ.",
"navigate": "Ńàvĩĝàţē",
"navigateBetweenPagesSearchTasksTrigger": "Ńàvĩĝàţē ƀēţŵēēń ƥàĝēś, śēàŕćĥ ţàśķś, ţŕĩĝĝēŕ ĝĩţ ōƥēŕàţĩōńś, àńď ḿàńàĝē ƥàńēĺś — àĺĺ ŵĩţĥōũţ ĺēàvĩńĝ ţĥē ķēŷƀōàŕď. Ćōńţēxţ-àŵàŕē ćōḿḿàńďś àƥƥēàŕ ƀàśēď ōń ţĥē àćţĩvē ƥàĝē.",
"newFolder": "Ńēŵ ƒōĺďēŕ",
"newFolderName": "Ńēŵ ƒōĺďēŕ ńàḿē",
"next": "Ńēxţ",
"noCliClientsInstalledYetInstall": "Ńō ĆĹĨ ćĺĩēńţś ĩńśţàĺĺēď ŷēţ. Ĩńśţàĺĺ Ćĺàũďē / Ćōďēx / ŌƥēńĆōďē / Àḿƥ ţō ēńàƀĺē ţĥĩś ƥĩćķēŕ.",
"noCommandsFound": "Ńō ćōḿḿàńďś ƒōũńď.",
"noContentMatchesFound": "Ńō ćōńţēńţ ḿàţćĥēś ƒōũńď.",
"noFilesFound": "Ńō ƒĩĺēś ƒōũńď.",
"noFoldersHerePickThisOne": "Ńō ƒōĺďēŕś ĥēŕē — ƥĩćķ ţĥĩś ōńē ōŕ ĝō ũƥ",
"officeIsNotEnabledForThis": "Ōƒƒĩćē ĩś ńōţ ēńàƀĺēď ƒōŕ ţĥĩś ŕũńţĩḿē.",
"officeRouteNotPortedYet": "Ţĥĩś Ōƒƒĩćē ŕōũţē ĩś ĥàńďĺēď ƀŷ ţĥē ŚƤÀ śĥēĺĺ, ƀũţ ĩţś ďēďĩćàţēď ćĺĩēńţ ƥàĝē ĩś śţĩĺĺ ƀēĩńĝ ƥōŕţēď: <1>{{pathname}}</1>",
"onboardingStepAgentsDescription": "Ḿàńàĝē ďĩśćōvēŕēď àĝēńţś àńď ĩńśţàĺĺ ńēŵ ōńēś.",
"onboardingStepAgentsTitle": "ÀĨ Àĝēńţś",
"onboardingStepCommandPanelDescription": "Qũĩćķ àććēśś ţō àćţĩōńś ƒŕōḿ àńŷŵĥēŕē ŵĩţĥ à ķēŷƀōàŕď śĥōŕţćũţ.",
"onboardingStepCommandPanelTitle": "Ćōḿḿàńď Ƥàńēĺ",
"onboardingStepExecutorsDescription": "Àĝēńţś ćàń ŕũń ĩń ďĩƒƒēŕēńţ ēxēćũţōŕ ēńvĩŕōńḿēńţś — ĺōćàĺ, ćōńţàĩńēŕĩźēď, ōŕ ŕēḿōţē.",
"onboardingStepExecutorsTitle": "Ēxēćũţōŕś",
"onboardingStepWorkflowsDescription": "Ŵōŕķƒĺōŵś ďēƒĩńē ţĥē śţēƥś àńď àũţōḿàţĩōń ƒōŕ ŷōũŕ ţàśķś.",
"onboardingStepWorkflowsTitle": "Àĝēńţĩć Ŵōŕķƒĺōŵś",
"open": "Ōƥēń",
"openFullPage": "Ōƥēń ƒũĺĺ ƥàĝē",
"openingLastTask": "Ōƥēńĩńĝ ĺàśţ ţàśķ…",
"openIssue": "Ōƥēń ĩśśũē",
"optionalTheRepositoryTheAgentWorks": "Ōƥţĩōńàĺ — ţĥē ŕēƥōśĩţōŕŷ ţĥē àĝēńţ ŵōŕķś ĩń.",
"panels": "Ƥàńēĺś",
"parentDirectory": "Ƥàŕēńţ ďĩŕēćţōŕŷ",
"permitTheCliToUploadCode": "Ƥēŕḿĩţ ţĥē ĆĹĨ ţō ũƥĺōàď ćōďē ƒōŕ ćĺōũď ĩńďēxĩńĝ (àũĝĝĩē / śĩḿĩĺàŕ).",
"pickACliClient": "Ƥĩćķ à ĆĹĨ ćĺĩēńţ",
"pickACliClientToLoad": "Ƥĩćķ à ĆĹĨ ćĺĩēńţ ţō ĺōàď ĩţś àvàĩĺàƀĺē ḿōďēĺś àńď ḿōďēś.",
@@ -85,6 +185,8 @@
"pluginPanelUnavailable": "Ţĥĩś ƥàńēĺ ĩś ńō ĺōńĝēŕ àvàĩĺàƀĺē.",
"plugins": "Ƥĺũĝĩńś",
"prDetails": "ƤŔ Ďēţàĩĺś",
"preparingKandevRepository": "Ƥŕēƥàŕĩńĝ ķàńďēv ŕēƥōśĩţōŕŷ…",
"preparingKandevRepositoryInBackground": "Ƥŕēƥàŕĩńĝ ķàńďēv ŕēƥōśĩţōŕŷ ĩń ƀàćķĝŕōũńď. Ƒĩĺĺ ĩń ţĥē ďēţàĩĺś, śũƀḿĩţ ŵĥēń ŕēàďŷ.",
"presetIconBug": "Ɓũĝ",
"presetIconCheck": "Ćĥēćķ",
"presetIconCode": "Ćōďē",
@@ -93,15 +195,22 @@
"presetIconSearch": "Śēàŕćĥ",
"presetIconSparkle": "Śƥàŕķĺē",
"presetIconTool": "Ţōōĺ",
"pressCmdKToOpenItAnytime": "<0>Ƥŕēśś</0><1></1><2>ţō ōƥēń ĩţ àńŷţĩḿē</2>",
"pressEnterToConfirm": "Ƥŕēśś Ēńţēŕ ţō ćōńƒĩŕḿ",
"profileName": "Ƥŕōƒĩĺē ńàḿē",
"prompts": "Ƥŕōḿƥţś",
"refreshAriaLabel": "Ŕēƒŕēśĥ {{label}}",
"refreshBranchesGitFetch": "Ŕēƒŕēśĥ ƀŕàńćĥēś (ĝĩţ ƒēţćĥ)",
"refreshingLabel": "Ŕēƒŕēśĥĩńĝ {{label}}...",
"refreshLabelLastFetched": "Ŕēƒŕēśĥ {{label}} (ĺàśţ ƒēţćĥēď {{time}})",
"reload": "Ŕēĺōàď",
"reloadTheApplicationToTryAgain": "Ŕēĺōàď ţĥē àƥƥĺĩćàţĩōń ţō ţŕŷ àĝàĩń.",
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
"rename": "Ŕēńàḿē",
"reviewSubmoduleBoundary": "Śũƀḿōďũĺē: {{scope}}",
"repository": "Ŕēƥōśĩţōŕŷ",
"repositoryName": "Ŕēƥōśĩţōŕŷ ńàḿē",
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
"reset": "Ŕēśēţ",
"resizeTableColumns": "Ŕēśĩźē ţàƀĺē ćōĺũḿńś {{left}} àńď {{right}}",
"resetting": "Ŕēśēţţĩńĝ…",
"resetWatchChecking": "Ćĥēćķĩńĝ ĥōŵ ḿàńŷ ţàśķś ŵōũĺď ƀē ďēĺēţēď…",
"resetWatchDeleteAllTasks": "Ţĥĩś ŵĩĺĺ ďēĺēţē ēvēŕŷ ţàśķ ƥŕēvĩōũśĺŷ ćŕēàţēď ƀŷ ţĥē ŵàţćĥ, ĩńćĺũďĩńĝ àŕćĥĩvēď ţàśķś. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
"resetWatchDeleteTasks_one": "Ţĥĩś ŵĩĺĺ ďēĺēţē {{count}} ţàśķ ƥŕēvĩōũśĺŷ ćŕēàţēď ƀŷ ţĥē ŵàţćĥ, ĩńćĺũďĩńĝ àŕćĥĩvēď ţàśķś. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
@@ -109,24 +218,70 @@
"resetWatchNoTasks": "Ńō ţàśķś ŵēŕē ćŕēàţēď ƀŷ ţĥĩś ŵàţćĥ ŷēţ — ōńĺŷ ţĥē ƥōĺĺĩńĝ śţàţē ŵĩĺĺ ƀē ćĺēàŕēď. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
"resetWatchPreviewFailedRequired": "Ćōũĺď ńōţ ĺōàď ţĥē àƒƒēćţēď ţàśķ ćōũńţ. Ŕēţŕŷ ţĥē ƥŕēvĩēŵ ƀēƒōŕē ŕēśēţţĩńĝ ţĥĩś ŵàţćĥ.",
"resetWatchTitle": "Ŕēśēţ {{label}}?",
"resetting": "Ŕēśēţţĩńĝ…",
"resizeTableColumns": "Ŕēśĩźē ţàƀĺē ćōĺũḿńś {{left}} àńď {{right}}",
"results": "Ŕēśũĺţś",
"retryPreview": "Ŕēţŕŷ ƥŕēvĩēŵ",
"reviewSubmoduleBoundary": "Śũƀḿōďũĺē: {{scope}}",
"runsFormatTypecheckTestAndLint": "Ŕũńś ƒōŕḿàţ, ţŷƥēćĥēćķ, ţēśţ, àńď ĺĩńţ àćŕōśś ţĥē ḿōńōŕēƥō.",
"runtimeDocker": "Ďōćķēŕ",
"runtimeDockerDescription": "Ćōńţàĩńēŕĩźēď ēxēćũţĩōń ƒōŕ ƒũĺĺ ĩśōĺàţĩōń àńď ŕēƥŕōďũćĩƀĩĺĩţŷ.",
"runtimeGitWorktree": "Ĝĩţ Ŵōŕķţŕēē",
"runtimeGitWorktreeDescription": "Ĩśōĺàţēď ƀŕàńćĥ ēńvĩŕōńḿēńţ ũńďēŕ à ŵōŕķţŕēē ŕōōţ ƒōŕ ƥàŕàĺĺēĺ ŵōŕķ.",
"runtimeLocal": "Ĺōćàĺ",
"runtimeLocalDescription": "Ŕũń àĝēńţś ďĩŕēćţĺŷ ōń ŷōũŕ ḿàćĥĩńē ŵĩţĥ ƒũĺĺ àććēśś ţō ŷōũŕ ĺōćàĺ ƒĩĺēśŷśţēḿ.",
"runtimeSprites": "Śƥŕĩţēś (Ŕēḿōţē śƥŕĩţēś.ďēv)",
"runtimeSpritesDescription": "Ĥàŕďŵàŕē-ĩśōĺàţēď ēxēćũţĩōń ēńvĩŕōńḿēńţ ƒōŕ àŕƀĩţŕàŕŷ ćōďē.",
"save": "Śàvē",
"scopeCommands": "Ćōḿḿàńďś",
"scopeContents": "Ćōńţēńţś",
"scopeFiles": "Ƒĩĺēś",
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
"scratchWorkspace": "śćŕàţćĥ ŵōŕķśƥàćē",
"searchFailedEditTheQueryOr": "Śēàŕćĥ ƒàĩĺēď. Ēďĩţ ţĥē qũēŕŷ ōŕ ŕēōƥēń śēàŕćĥ ţō ŕēţŕŷ.",
"searchForFiles": "Śēàŕćĥ ƒōŕ ƒĩĺēś...",
"searchForTasks": "Śēàŕćĥ ƒōŕ ţàśķś...",
"searchingTaskWorkspace": "Śēàŕćĥĩńĝ ţàśķ ŵōŕķśƥàćē…",
"searchQueriesAreLimitedTo200": "Śēàŕćĥ qũēŕĩēś àŕē ĺĩḿĩţēď ţō 200 ćĥàŕàćţēŕś.",
"searchTaskContents": "Śēàŕćĥ ţàśķ ćōńţēńţś…",
"select": "Śēĺēćţ",
"settings": "Śēţţĩńĝś",
"shell": "Śĥēĺĺ",
"shellCommandPlusSlashCommandSkills": "Śĥēĺĺ ćōḿḿàńď ŷōũ ćàń ŕũń ĩń ţĥē śēćōńďàŕŷ ĩńśţàńćē, ƥĺũś śĺàśĥ-ćōḿḿàńď <2>śķĩĺĺś</2> ŷōũ ćàń àśķ ţĥē àĝēńţ ţō ŕũń ďũŕĩńĝ ţĥē ŵōŕķƒĺōŵ.",
"showAdvancedOptions": "Śĥōŵ àďvàńćēď ōƥţĩōńś",
"skip": "Śķĩƥ",
"stagesAndCommitsChangesUsingConventional": "Śţàĝēś àńď ćōḿḿĩţś ćĥàńĝēś ũśĩńĝ Ćōńvēńţĩōńàĺ Ćōḿḿĩţś.",
"status": "Śţàţũś",
"subtaskTitle": "Śũƀţàśķ ţĩţĺē",
"switchMode": "Śŵĩţćĥ ḿōďē",
"system": "Śŷśţēḿ",
"taskLoading": "Ĺōàďĩńĝ ţàśķ...",
"tasks": "Ţàśķś",
"taskUnavailable": "Ţàśķ ũńàvàĩĺàƀĺē",
"taskUnavailableDescription": "Ŵē ćōũĺď ńōţ ĺōàď ţĥĩś ţàśķ. Ĩţ ḿàŷ ĥàvē ƀēēń ďēĺēţēď ōŕ ŷōũ ḿàŷ ńōţ ĥàvē àććēśś.",
"theAgentForksKandevToYourAccount": "Ţĥē àĝēńţ ƒōŕķś <1>{{repo}}</1> ţō ŷōũŕ ĜĩţĤũƀ àććōũńţ àńď ōƥēńś à ƤŔ ƒŕōḿ ŷōũŕ ƒōŕķ, ćŕēďĩţēď ţō ŷōũ",
"theBaseBranchTheAgentStarts": "Ţĥē ƀàśē ƀŕàńćĥ ţĥē àĝēńţ śţàŕţś ƒŕōḿ.",
"theFinalStepOpensAPullRequest": "Ţĥē ƒĩńàĺ śţēƥ ōƒ ţĥĩś ŵōŕķƒĺōŵ ōƥēńś à ƥũĺĺ ŕēqũēśţ, ŵĥĩćĥ ńēēďś ţĥē <1>{{binary}}</1> ĆĹĨ ţō ƀē àũţĥēńţĩćàţēď. {{message}}",
"thisEmptyRepositoryWillRunWith": "Ţĥĩś ēḿƥţŷ ŕēƥōśĩţōŕŷ ŵĩĺĺ ŕũń ŵĩţĥ “{{profile}}” ōń ţĥĩś ḿàćĥĩńē.",
"thisGithubAccountCannotForkKdlbs": "Ţĥĩś ĜĩţĤũƀ àććōũńţ ćàńńōţ ƒōŕķ ķďĺƀś/ķàńďēv.",
"thisPageCouldNotLoad": "Ţĥĩś ƥàĝē ćōũĺďń’ţ ĺōàď.",
"thisWorkflowWillOnlyCreateA": "Ţĥĩś ŵōŕķƒĺōŵ ŵĩĺĺ ōńĺŷ ćŕēàţē à ĜĩţĤũƀ ĩśśũē ƒŕōḿ ţĥē ŕēƥōśĩţōŕŷ ţēḿƥĺàţē. Ţĥē àĝēńţ ŵĩĺĺ àśķ ƒōŕ ḿĩśśĩńĝ ďēţàĩĺś àńď ƥũƀĺĩćàţĩōń àƥƥŕōvàĺ; ĩţ ŵĩĺĺ ńōţ ĩḿƥĺēḿēńţ ţĥē ćĥàńĝē ōŕ ōƥēń à ƥũĺĺ ŕēqũēśţ.",
"title": "Ţĩţĺē",
"toastRegionLabel": "Ńōţĩƒĩćàţĩōńś",
"typeACommand": "Ţŷƥē à ćōḿḿàńď...",
"typeToSearchFiles": "Ţŷƥē ţō śēàŕćĥ ƒĩĺēś...",
"typeToSearchTaskContents": "Ţŷƥē ţō śēàŕćĥ ţàśķ ćōńţēńţś…",
"unknownPanel": "Ũńķńōŵń ƥàńēĺ: {{component}}",
"unsavedChanges": "Ũńśàvēď ćĥàńĝēś",
"usefulCommandsAgentSkills": "Ũśēƒũĺ ćōḿḿàńďś & àĝēńţ śķĩĺĺś",
"useScratchInstead": "Ũśē śćŕàţćĥ ĩńśţēàď",
"useThisFolder": "Ũśē ţĥĩś ƒōĺďēŕ",
"waitsForCiAndAutomatedReviews": "Ŵàĩţś ƒōŕ ĆĨ àńď àũţōḿàţēď ŕēvĩēŵś, ţĥēń ƒĩxēś ƒàĩĺũŕēś àńď àďďŕēśśēś ćōḿḿēńţś.",
"whenItsDoneTheAgentOpensAPr": "Ŵĥēń ĩţ'ś ďōńē, ţĥē àĝēńţ ōƥēńś à ƥũĺĺ ŕēqũēśţ ţō <2>{{repo}}</2> ƒōŕ ţĥē ḿàĩńţàĩńēŕś ţō ŕēvĩēŵ, śàvĩńĝ ţĥēḿ ţĩḿē àńď śĥĩƥƥĩńĝ ţĥē ĩḿƥŕōvēḿēńţ ţō ēvēŕŷōńē.",
"workflow": "Ŵōŕķƒĺōŵ",
"workflowsControlTheStepsAutomationAnd": "Ŵōŕķƒĺōŵś ćōńţŕōĺ ţĥē śţēƥś, àũţōḿàţĩōń, àńď àĝēńţ ƀēĥàvĩōŕ ƒōŕ ŷōũŕ ţàśķś. Ŷōũ ćàń àďď ḿōŕē ŵōŕķƒĺōŵś ƒŕōḿ Śēţţĩńĝś.",
"workspace": "Ŵōŕķśƥàćē",
"workspaces": "Ŵōŕķśƥàćēś",
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń."
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń.",
"yourAgentImplementsItInThe": "Ŷōũŕ àĝēńţ ĩḿƥĺēḿēńţś ĩţ ĩń ţĥē ķàńďēv ŕēƥōśĩţōŕŷ, ŵĩţĥ ţēśţś",
"youVerifyAndTestTheChange": "Ŷōũ vēŕĩƒŷ àńď ţēśţ ţĥē ćĥàńĝē ĩń à śēćōńď ķàńďēv ĩńśţàńćē"
}
+27 -4
View File
@@ -2,31 +2,38 @@
"addDetailsAboutThisChange": "Àďď ďēţàĩĺś àƀōũţ ţĥĩś ćĥàńĝē...",
"allRepos": "Àĺĺ ŕēƥōś",
"anErrorOccurred": "Àń ēŕŕōŕ ōććũŕŕēď",
"authRequiredHeading": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď",
"authRequiredInline": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď.",
"authenticated": "Àũţĥēńţĩćàţēď",
"authenticationFailed": "Àũţĥēńţĩćàţĩōń ƒàĩĺēď: {{error}}",
"authRequiredHeading": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď",
"authRequiredInline": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď.",
"branchPushedNotCreated": "Ɓŕàńćĥ ƥũśĥēď; {{shortName}} ńōţ ćŕēàţēď",
"branchWasPushedRetryCreation": "Ɓŕàńćĥ ŵàś ƥũśĥēď; ŕēţŕŷ {{longName}} ćŕēàţĩōń.",
"checkedRelative": "· ćĥēćķēď {{relative}}",
"commit": "Ćōḿḿĩţ",
"commitChangedFiles_one": "Ćōḿḿĩţ {{count}} ćĥàńĝēď ƒĩĺē",
"commitChangedFiles_other": "Ćōḿḿĩţ {{count}} ćĥàńĝēď ƒĩĺēś",
"commitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
"commitChangesScoped": "Ćōḿḿĩţ Ćĥàńĝēś — {{scopedRepo}}",
"commitsAheadAriaLabel": "{{value}} ćōḿḿĩţś àĥēàď",
"commitsBehindAriaLabel": "{{value}} ćōḿḿĩţś ƀēĥĩńď",
"committingEllipsis": "Ćōḿḿĩţţĩńĝ...",
"configureAUtilityAgentInSettings": "Ćōńƒĩĝũŕē à ũţĩĺĩţŷ àĝēńţ ĩń śēţţĩńĝś ţō ēńàƀĺē ÀĨ ĝēńēŕàţĩōń",
"copiedConfigTo": "Ćōƥĩēď {{label}} ćōńƒĩĝ ţō {{targetName}}.",
"copyConfig": "Ćōƥŷ ćōńƒĩĝ",
"copyConfigSettingsOnly": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
"copyConfigToAnotherWorkspace": "Ćōƥŷ ćōńƒĩĝ ţō àńōţĥēŕ ŵōŕķśƥàćē",
"copyConfigWithCredentials": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś àńď ćŕēďēńţĩàĺś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
"copyConfiguration": "Ćōƥŷ {{label}} ćōńƒĩĝũŕàţĩōń",
"copyConfigWithCredentials": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś àńď ćŕēďēńţĩàĺś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
"copyingConfigTo": "Ćōƥŷĩńĝ {{label}} ćōńƒĩĝ ţō {{targetName}}…",
"createAsDraft": "Ćŕēàţē àś ďŕàƒţ",
"createdSuccessfully": "{{longName}} ćŕēàţēď śũććēśśƒũĺĺŷ",
"createFailed": "Ćŕēàţē {{shortName}} ƒàĩĺēď",
"createLongName": "Ćŕēàţē {{longName}}",
"createPr": "Ćŕēàţē ƤŔ",
"createPrCommitsAhead_one": "Ćŕēàţē ƤŔ ({{count}} ćōḿḿĩţ àĥēàď)",
"createPrCommitsAhead_other": "Ćŕēàţē ƤŔ ({{count}} ćōḿḿĩţś àĥēàď)",
"createScoped": "Ćŕēàţē {{longName}} — {{scopedRepo}}",
"createShortName": "Ćŕēàţē {{shortName}}",
"createdSuccessfully": "{{longName}} ćŕēàţēď śũććēśśƒũĺĺŷ",
"creatingEllipsis": "Ćŕēàţĩńĝ...",
"creatingFrom": "Ćŕēàţĩńĝ {{shortName}} ƒŕōḿ <4>{{displayBranch}}</4>",
"creatingFromInto": "Ćŕēàţĩńĝ {{shortName}} ƒŕōḿ <4>{{displayBranch}}</4> → <6>{{baseBranch}}</6>",
@@ -40,6 +47,8 @@
"failedToCopyConfig": "Ƒàĩĺēď ţō ćōƥŷ ćōńƒĩĝ: {{err}}",
"filesChanged_one": "<0>{{count}}</0> ƒĩĺē ćĥàńĝēď",
"filesChanged_other": "<0>{{count}}</0> ƒĩĺēś ćĥàńĝēď",
"forcePush": "Ƒōŕćē Ƥũśĥ",
"fromBranch": "ƒŕōḿ {{branch}}",
"generateCommitDescriptionWithAi": "Ĝēńēŕàţē ćōḿḿĩţ ďēśćŕĩƥţĩōń ŵĩţĥ ÀĨ",
"generateCommitMessageWithAi": "Ĝēńēŕàţē ćōḿḿĩţ ḿēśśàĝē ŵĩţĥ ÀĨ",
"generateDescriptionWithAi": "Ĝēńēŕàţē {{shortName}} ďēśćŕĩƥţĩōń ŵĩţĥ ÀĨ",
@@ -47,8 +56,22 @@
"loading": "Ĺōàďĩńĝ…",
"longNameTitle": "{{longName}} ţĩţĺē",
"longNameTitlePlaceholder": "{{longName}} ţĩţĺē...",
"merge": "Ḿēŕĝē",
"noChangesToCommit": "Ńō ćĥàńĝēś ţō ćōḿḿĩţ",
"noSavedQueriesYet": "Ńō śàvēď qũēŕĩēś ŷēţ",
"ontoBranch": "ōńţō {{branch}}",
"openVcsOptions": "Ōƥēń VĆŚ ōƥţĩōńś",
"pickActionThenRepo": "Ƥĩćķ àćţĩōń, ţĥēń ŕēƥō",
"pickARepositoryForCommit": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ćōḿḿĩţ",
"pickARepositoryForCreatePr": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ćŕēàţē ƤŔ",
"pickARepositoryForPush": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ƥũśĥ",
"pickARepositoryForRebase": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ŕēƀàśē",
"pull": "Ƥũĺĺ",
"push": "Ƥũśĥ",
"pushCommitsToRemote_one": "Ƥũśĥ {{count}} ćōḿḿĩţ ţō ŕēḿōţē",
"pushCommitsToRemote_other": "Ƥũśĥ {{count}} ćōḿḿĩţś ţō ŕēḿōţē",
"rebase": "Ŕēƀàśē",
"rebaseOntoBranchBehind": "Ŕēƀàśē ōńţō {{branch}} ({{behind}} ƀēĥĩńď)",
"reconnectProvider": "Ŕēćōńńēćţ {{name}}",
"repository": "Ŕēƥōśĩţōŕŷ",
"retryShortName": "Ŕēţŕŷ {{shortName}}",
@@ -225,6 +225,7 @@
"wipLimit": "ŴĨƤ ĺĩḿĩţ",
"wipLimitHelp": "Ḿàxĩḿũḿ àďḿĩţţēď (àćţĩvē) ţàśķś ĩń ţĥĩś śţēƥ. Ōvēŕƒĺōŵ śţàŷś vĩśĩƀĺē ĩń à qũēũē. Ũśē 0 ƒōŕ ũńĺĩḿĩţēď.",
"wipLimitsNote": "ŴĨƤ ĺĩḿĩţś àćţĩvē ŵōŕķ, ńōţ vĩśĩƀĩĺĩţŷ. Ōvēŕƒĺōŵ ŕēḿàĩńś ōń ţĥē ƀōàŕď ũńţĩĺ ćàƥàćĩţŷ ōƥēńś; ĩƒ à śēĺēćţēď ƒēēďēŕ ĩś àĺśō ƒũĺĺ, ńēŵ ţàśķ ćŕēàţĩōń ĩś ŕēĵēćţēď.",
"workflow": "Ŵōŕķƒĺōŵ",
"workflowCycleBlocked": "Ŵōŕķƒĺōŵ ćŷćĺē ƀĺōćķēď",
"workflowCycleBlockedDescription": "Ćĥàńĝē ţĥē ŵōŕķƒĺōŵ śţēƥś ţō ŕēḿōvē ţĥē àũţōḿàţĩć ćŷćĺē ƀēƒōŕē ćōńţĩńũĩńĝ.",
"workflowCycleBlockingBody": "Ţĥĩś ƥàţĥ ŕē-ēńţēŕś \"{{stepName}}\" àńď ćàń śţàŕţ ţĥē àĝēńţ àĝàĩń ŵĩţĥōũţ àńōţĥēŕ ũśēŕ àćţĩōń.",