fix(i18n): close four gaps no gate can see (plurals, fallback copy, orphans) (#2388)

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-08-07 20:03:10 +02:00
committed by GitHub
parent 312dc0657f
commit 97fba4ec4b
25 changed files with 327 additions and 34 deletions
@@ -224,6 +224,38 @@ it("uses the generated description when an action prompt is blank", () => {
expect(screen.getByTestId("task-description").textContent).toContain(
"Azure DevOps work item: 73",
);
// The fallback is UI copy the user reads (and can edit) in the create dialog,
// so it resolves through the catalog rather than a bare literal. The English
// is unchanged.
expect(screen.getByTestId("task-description").textContent).toContain("(no description)");
});
it("strips markup from a work-item description instead of using the fallback", () => {
render(
<AzureDevOpsTaskLauncher
workspaceId={workspaceId}
workflows={[workflow]}
steps={[step]}
repositories={[repository]}
payload={{
kind: "work-item",
item: {
id: 74,
revision: 1,
title: "Has a description",
type: "Issue",
state: "To Do",
project: "project-1",
description: "<div>Real <b>description</b></div>",
},
}}
onClose={mocks.close}
/>,
);
const text = screen.getByTestId("task-description").textContent ?? "";
expect(text).toContain("Real description");
expect(text).not.toContain("(no description)");
});
it("reports a missing work-item project after creating the task", async () => {
@@ -18,15 +18,19 @@ import type {
} from "@/lib/types/azure-devops";
import type { Repository, Task, Workflow, WorkflowStep } from "@/lib/types/http";
import { truncateRemoteTaskTitle } from "@/lib/task-title";
import { t } from "@/lib/i18n";
export type AzureDevOpsLaunchPayload =
| { kind: "work-item"; item: AzureDevOpsWorkItem; action?: AzureDevOpsActionPreset }
| { kind: "pull-request"; pullRequest: AzureDevOpsPullRequest; action?: AzureDevOpsActionPreset };
function plainText(value: string | undefined): string {
if (!value) return "(no description)";
// Resolved per call, not at module load, so the fallback follows the active
// locale. `launchText` runs inside the launcher's `useMemo`, i.e. at render.
const fallback = t("azuredevops:descriptionFallback");
if (!value) return fallback;
const document = new DOMParser().parseFromString(value, "text/html");
return document.body.textContent?.trim() || "(no description)";
return document.body.textContent?.trim() || fallback;
}
function launchText(payload: AzureDevOpsLaunchPayload) {
+1 -1
View File
@@ -432,7 +432,7 @@ function KanbanCardActions({
<div className="flex items-center gap-2">
{renderSubagentCountChip(
task,
t("activeSubagents", { count: task.activeSubagentCount ?? 0 }),
t("common:activeSubagents", { count: task.activeSubagentCount ?? 0 }),
)}
{statusIcon}
{showMaximizeButton && onOpenFullPage && hasKnownSession && (
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { GetTaskPlanRenderer, UpdateTaskPlanRenderer } from "./document-renderers";
function plan(content: string | undefined) {
return renderToStaticMarkup(
<GetTaskPlanRenderer args={{ task_id: "t1" }} result={{ content }} status="complete" />,
);
}
function updatedPlan(content: string | undefined) {
return renderToStaticMarkup(
<UpdateTaskPlanRenderer args={{ task_id: "t1" }} result={{ content }} status="complete" />,
);
}
// The plan/document summary carries two counts. i18next selects a plural form on
// `count` alone, so lines own `count` and the character total rides along as a
// plain interpolated value. English must stay byte-identical.
describe("document summary", () => {
it("uses the singular line label for single-line content", () => {
expect(plan("one line")).toContain("1 line · 8 chars");
});
it("uses the plural line label for multi-line content", () => {
expect(plan("a\nb\nc")).toContain("3 lines · 5 chars");
});
it("keeps 'chars' out of the plural decision", () => {
// A single character with several lines must still pluralise on lines only,
// matching the pre-i18n string exactly.
expect(plan("\n\n")).toContain("3 lines · 2 chars");
});
it("falls back to the empty label when there is no content", () => {
expect(updatedPlan(undefined)).toContain("empty");
});
});
@@ -17,6 +17,7 @@ import { EmptyListNote, IdChip, KandevBody, KandevRow, KeyValueRow, SummaryDot }
import { pickArray, pickNumber, pickString } from "./parse";
import type { KandevRenderer } from "./types";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
// MarkdownBody renders task plan / document content. We pre-trim and use the
// shared markdown component set so heading sizes, code blocks, and mermaid
@@ -43,11 +44,13 @@ function ContentBox({ children }: { children: ReactNode }) {
);
}
function summarizeContent(content: string | undefined): string {
if (!content) return "empty";
// Two counts in one label, but i18next selects a plural form on `count` alone.
// Lines own `count`; the character total rides along as a plain value.
function summarizeContent(t: TFunction, content: string | undefined): string {
if (!content) return t("task:documentSummaryEmpty");
const lines = content.split("\n").length;
const chars = content.length;
return `${lines} line${lines === 1 ? "" : "s"} · ${chars} chars`;
return t("task:documentSummary", { count: lines, chars });
}
// ---------- get_task_plan ----------
@@ -70,7 +73,7 @@ export const GetTaskPlanRenderer: KandevRenderer = ({ args, result, status }) =>
<SummaryDot />
</>
)}
<span>{hasPlan ? summarizeContent(content) : t("task:noPlan")}</span>
<span>{hasPlan ? summarizeContent(t, content) : t("task:noPlan")}</span>
</span>
}
status={status}
@@ -153,7 +156,7 @@ export const UpdateTaskPlanRenderer: KandevRenderer = ({ args, result, status })
<SummaryDot />
</>
)}
<span>{summarizeContent(displayContent)}</span>
<span>{summarizeContent(t, displayContent)}</span>
</span>
}
status={status}
@@ -46,7 +46,9 @@ type MonitorViewModel = {
function pluralizeEventCount(n: number): string {
if (n <= 0) return "";
return ` · ${n} event${n === 1 ? "" : "s"}`;
// The leading separator stays in code — it is layout punctuation between the
// title and the count, not copy a translator should have to reproduce.
return ` · ${t("task:eventCount", { count: n })}`;
}
function buildMonitorViewModel(comment: Message): MonitorViewModel {
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { Message } from "@/lib/types/http";
import { ToolEditMessage } from "./tool-edit-message";
function writeComment(content: string, body: string): Message {
return {
id: "m1",
content,
metadata: {
status: "complete",
normalized: {
modify_file: {
file_path: "apps/web/lib/utils.ts",
mutations: [{ type: "create", content: body, start_line: 1 }],
},
},
},
} as unknown as Message;
}
// The write-card header used to append "(N line(s))" with a template-literal
// plural. It now selects _one/_other, with the path kept out of the plural
// decision and passed through as an interpolated value.
describe("ToolEditMessage write summary", () => {
it("uses the singular line suffix for a one-line write", () => {
const html = renderToStaticMarkup(
<ToolEditMessage comment={writeComment("Write utils.ts", "only line")} />,
);
expect(html).toContain("Write utils.ts (1 line)");
expect(html).not.toContain("(1 lines)");
});
it("uses the plural line suffix for a multi-line write", () => {
const html = renderToStaticMarkup(
<ToolEditMessage comment={writeComment("Write utils.ts", "a\nb\nc")} />,
);
expect(html).toContain("Write utils.ts (3 lines)");
});
it("leaves the summary untouched for an edit that is not a write", () => {
const comment = {
id: "m1",
content: "Edit utils.ts",
metadata: {
status: "complete",
normalized: {
modify_file: {
file_path: "apps/web/lib/utils.ts",
mutations: [{ type: "edit", old_string: "a", new_string: "b", start_line: 1 }],
},
},
},
} as unknown as Message;
const html = renderToStaticMarkup(<ToolEditMessage comment={comment} />);
expect(html).toContain("Edit utils.ts");
expect(html).not.toContain("line)");
});
});
@@ -19,6 +19,7 @@ import { transformFileMutation, type FileMutation } from "@/lib/diff";
import { useExpandState } from "./use-expand-state";
import { useOpenFileAtLine } from "@/hooks/use-file-editors";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type ModifyFilePayload = {
file_path?: string;
@@ -48,6 +49,7 @@ function EditStatusIcon({ status }: { status: string | undefined }) {
// getEditSummary returns the short header label for an edit/write card.
function getEditSummary(
t: TFunction,
content: string,
worktreePath: string | undefined,
isWriteOperation: boolean,
@@ -55,7 +57,7 @@ function getEditSummary(
): string {
const baseSummary = transformPathsInText(content, worktreePath);
if (isWriteOperation && lineCount > 0) {
return `${baseSummary} (${lineCount} line${lineCount !== 1 ? "s" : ""})`;
return t("task:editSummaryLines", { summary: baseSummary, count: lineCount });
}
return baseSummary;
}
@@ -166,6 +168,7 @@ export const ToolEditMessage = memo(function ToolEditMessage({
sessionId,
onOpenFile,
}: ToolEditMessageProps) {
const { t } = useTranslation();
const {
status,
filePath,
@@ -181,7 +184,7 @@ export const ToolEditMessage = memo(function ToolEditMessage({
const autoExpanded = status === "running";
const { isExpanded, handleToggle } = useExpandState(status, autoExpanded);
const Icon = isWriteOperation ? IconFilePlus : IconEdit;
const summary = getEditSummary(comment.content, worktreePath, isWriteOperation, lineCount);
const summary = getEditSummary(t, comment.content, worktreePath, isWriteOperation, lineCount);
const handleCopyPath = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -24,12 +24,46 @@ function readComment(filePath: string, offset?: number, limit?: number): Message
} as unknown as Message;
}
function readCommentWithLines(lineCount?: number): Message {
return {
id: "m1",
metadata: {
status: "complete",
normalized: {
read_file: {
file_path: "apps/web/lib/read-selector.ts",
output: { line_count: lineCount },
},
},
},
} as unknown as Message;
}
describe("ToolReadMessage", () => {
afterEach(() => {
cleanup();
useOpenFileAtLine.mockClear();
});
// The header label used to build its own "s" in a template literal, which no
// i18n gate could see. It now selects _one/_other; the English must be
// unchanged, because e2e specs query this card by its accessible name.
it("uses the singular header label for a one-line read", () => {
render(<ToolReadMessage comment={readCommentWithLines(1)} />);
expect(screen.getByText("Read 1 line")).toBeTruthy();
expect(screen.queryByText("Read 1 lines")).toBeNull();
});
it("uses the plural header label for a multi-line read", () => {
render(<ToolReadMessage comment={readCommentWithLines(137)} />);
expect(screen.getByText("Read 137 lines")).toBeTruthy();
});
it("falls back to the bare 'Read' label when no line count is reported", () => {
render(<ToolReadMessage comment={readCommentWithLines(undefined)} />);
expect(screen.getByText("Read")).toBeTruthy();
});
it("forwards the message session when opening a file at its read line", () => {
const openFile = vi.fn();
render(
@@ -10,6 +10,7 @@ import { useExpandState } from "./use-expand-state";
import { useOpenFileAtLine } from "@/hooks/use-file-editors";
import { splitReadFiles, type ReadFileRef } from "@/lib/read-selector";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type ReadFileOutput = {
content?: string;
@@ -48,9 +49,9 @@ function ReadStatusIcon({ status }: { status: string | undefined }) {
}
// getReadSummary returns the short "Read N lines" header label for the card.
function getReadSummary(lineCount: number | undefined): string {
if (lineCount) return `Read ${lineCount} line${lineCount !== 1 ? "s" : ""}`;
return "Read";
function getReadSummary(t: TFunction, lineCount: number | undefined): string {
if (lineCount) return t("task:readLines", { count: lineCount });
return t("task:read");
}
// formatLineRange renders the range the agent read (carried separately from
@@ -144,7 +145,7 @@ export const ToolReadMessage = memo(function ToolReadMessage({
<div className="flex items-center gap-2 text-xs min-w-0">
<span className="inline-flex items-center gap-1.5 shrink-0 whitespace-nowrap">
<span className="font-mono text-xs text-muted-foreground">
{getReadSummary(readOutput?.line_count)}
{getReadSummary(t, readOutput?.line_count)}
</span>
{!isSuccess && <ReadStatusIcon status={status} />}
</span>
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { Message } from "@/lib/types/http";
import { ToolSearchMessage } from "./tool-search-message";
function searchComment(files?: string[], fileCount?: number): Message {
return {
id: "m1",
metadata: {
status: "complete",
normalized: {
code_search: { pattern: "useTranslation", output: { files, file_count: fileCount } },
},
},
} as unknown as Message;
}
// The header label used to build its own "s" in a template literal, which the
// jsx-only lint rule and the inline-plural check both miss. It now selects
// _one/_other, and the English has to stay byte-identical.
describe("ToolSearchMessage", () => {
it("uses the singular header label for a single hit", () => {
const html = renderToStaticMarkup(<ToolSearchMessage comment={searchComment(["a.ts"])} />);
expect(html).toContain("Found 1 file");
expect(html).not.toContain("Found 1 files");
});
it("uses the plural header label for several hits", () => {
const html = renderToStaticMarkup(
<ToolSearchMessage comment={searchComment(["a.ts", "b.ts", "c.ts"])} />,
);
expect(html).toContain("Found 3 files");
});
it("prefers the reported file_count over the returned file list length", () => {
// The backend truncates `files` but keeps the true total in `file_count`,
// so the label must plural-select on the count it actually renders.
const html = renderToStaticMarkup(<ToolSearchMessage comment={searchComment(["a.ts"], 12)} />);
expect(html).toContain("Found 12 files");
});
it("falls back to the searching label before any result arrives", () => {
const html = renderToStaticMarkup(<ToolSearchMessage comment={searchComment([])} />);
expect(html).toContain("Searching");
expect(html).not.toContain("Found");
});
});
@@ -9,6 +9,7 @@ import type { Message } from "@/lib/types/http";
import { ExpandableRow } from "./expandable-row";
import { useExpandState } from "./use-expand-state";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
type CodeSearchOutput = {
files?: string[];
@@ -44,12 +45,12 @@ function SearchStatusIcon({ status }: { status: string | undefined }) {
return null;
}
function getSearchSummary(searchOutput: CodeSearchOutput | undefined): string {
function getSearchSummary(t: TFunction, searchOutput: CodeSearchOutput | undefined): string {
if (searchOutput?.files && searchOutput.files.length > 0) {
const count = searchOutput.file_count || searchOutput.files.length;
return `Found ${count} file${count !== 1 ? "s" : ""}`;
return t("task:foundFiles", { count });
}
return "Searching";
return t("task:toolSearching");
}
type SearchResultsProps = {
@@ -98,6 +99,7 @@ export const ToolSearchMessage = memo(function ToolSearchMessage({
worktreePath,
onOpenFile,
}: ToolSearchMessageProps) {
const { t } = useTranslation();
const { status, searchOutput, searchPath, searchPattern, hasOutput, isSuccess } =
parseSearchMetadata(comment);
const autoExpanded = status === "running";
@@ -110,7 +112,7 @@ export const ToolSearchMessage = memo(function ToolSearchMessage({
<div className="flex items-center gap-2 text-xs">
<span className="inline-flex items-center gap-1.5">
<span className="font-mono text-xs text-muted-foreground">
{getSearchSummary(searchOutput)}
{getSearchSummary(t, searchOutput)}
</span>
{!isSuccess && <SearchStatusIcon status={status} />}
</span>
@@ -370,7 +370,7 @@ export const TurnGroupMessage = memo(function TurnGroupMessage({
// calls and nothing else.
const rawDescription = isGroupRunning
? getActiveGroupDescription(group.messages)
: t("turnGroupToolCalls", { count: group.messages.length });
: t("chat:turnGroupToolCalls", { count: group.messages.length });
const description = transformPathsInText(rawDescription, worktreePath);
const count = group.messages.length;
@@ -266,7 +266,7 @@ export function useChatInputContainer(params: UseChatInputContainerParams) {
placeholder,
isAgentBusy,
hasAgentCommands,
steerPlaceholder: supportsSteering ? t("composerSteerPlaceholder") : undefined,
steerPlaceholder: supportsSteering ? t("chat:composerSteerPlaceholder") : undefined,
});
return {
+1 -1
View File
@@ -117,7 +117,7 @@ export const TaskSwitcher = memo(function TaskSwitcher(props: TaskSwitcherProps)
return (
<>
{loadErrorNotice}
<div className="px-3 py-3 text-xs text-muted-foreground">{t("noTasksYet")}</div>
<div className="px-3 py-3 text-xs text-muted-foreground">{t("sidebar:noTasksYet")}</div>
</>
);
}
@@ -99,7 +99,7 @@ describe("useSidebarMultiSelect", () => {
});
});
describe("useSidebarMultiSelect — bulk actions", () => {
describe("useSidebarMultiSelect — bulk archive", () => {
it("bulkArchive removes all on full success and clears the selection", async () => {
const { result } = renderHook(() => useSidebarMultiSelect("ws1"));
await act(async () => {
@@ -121,7 +121,24 @@ describe("useSidebarMultiSelect — bulk actions", () => {
});
expect(removeTasksFromStore).toHaveBeenCalledWith(new Set(["a"]));
expect(result.current.selectedIds).toEqual(new Set(["b"]));
expect(toast).toHaveBeenCalledWith(expect.objectContaining({ variant: "error" }));
// The title used to build its own "s" in a template literal. It now resolves
// through _one/_other, and the English must be unchanged.
expect(toast).toHaveBeenCalledWith({
title: "Failed to archive 1 task",
variant: "error",
});
});
it("bulkArchive pluralises the failure toast when several tasks fail", async () => {
archiveTaskById.mockImplementation(() => Promise.reject(new Error("nope")));
const { result } = renderHook(() => useSidebarMultiSelect("ws1"));
await act(async () => {
await result.current.bulkArchive(["a", "b"]);
});
expect(toast).toHaveBeenCalledWith({
title: "Failed to archive 2 tasks",
variant: "error",
});
});
it("bulkArchive routes the active task through the switch-aware path", async () => {
@@ -143,7 +160,9 @@ describe("useSidebarMultiSelect — bulk actions", () => {
});
expect(archiveTaskById).not.toHaveBeenCalled();
});
});
describe("useSidebarMultiSelect — bulk delete", () => {
it("bulkDelete removes all on full success and clears the selection", async () => {
const { result } = renderHook(() => useSidebarMultiSelect("ws1"));
await act(async () => {
@@ -164,7 +183,22 @@ describe("useSidebarMultiSelect — bulk actions", () => {
await result.current.bulkDelete(["a", "b"]);
});
expect(result.current.selectedIds).toEqual(new Set(["b"]));
expect(toast).toHaveBeenCalledWith(expect.objectContaining({ variant: "error" }));
expect(toast).toHaveBeenCalledWith({
title: "Failed to delete 1 task",
variant: "error",
});
});
it("bulkDelete pluralises the failure toast when several tasks fail", async () => {
deleteTaskById.mockImplementation(() => Promise.reject(new Error("nope")));
const { result } = renderHook(() => useSidebarMultiSelect("ws1"));
await act(async () => {
await result.current.bulkDelete(["a", "b"]);
});
expect(toast).toHaveBeenCalledWith({
title: "Failed to delete 2 tasks",
variant: "error",
});
});
it("bulkDelete routes the active task through the switch-aware removal", async () => {
+7 -5
View File
@@ -11,6 +11,7 @@ import { useTaskRemoval } from "./use-task-removal";
import { useTaskWorkflowMove } from "./use-task-workflow-move";
import { useToast } from "@/components/toast-provider";
import { useAppStoreApi } from "@/components/state-provider";
import { useTranslation } from "react-i18next";
type BulkOpts = { cascade?: boolean };
@@ -31,6 +32,7 @@ function useSidebarBulkActions(
const { getWorkflowIdForTask, removeTasksFromStore } = useTaskMultiSelectStore();
const moveTasks = useTaskWorkflowMove();
const { toast } = useToast();
const { t } = useTranslation();
const [isArchiving, setIsArchiving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -40,7 +42,7 @@ function useSidebarBulkActions(
per: (id: string) => Promise<void>,
handleActive: (id: string) => Promise<void>,
setBusy: (v: boolean) => void,
noun: string,
failureKey: string,
) => {
if (ids.length === 0) return;
const activeId = store.getState().tasks.activeTaskId;
@@ -63,7 +65,7 @@ function useSidebarBulkActions(
// Keep the failed ids selected so the user can retry, and surface it.
dispatch({ type: "set_selected", ids: new Set(failed) });
toast({
title: `Failed to ${noun} ${failed.length} task${failed.length === 1 ? "" : "s"}`,
title: t(failureKey, { count: failed.length }),
variant: "error",
});
return;
@@ -73,7 +75,7 @@ function useSidebarBulkActions(
setBusy(false);
}
},
[store, removeTasksFromStore, dispatch, toast, clearSelection],
[store, removeTasksFromStore, dispatch, toast, clearSelection, t],
);
const bulkArchive = useCallback(
@@ -83,7 +85,7 @@ function useSidebarBulkActions(
(id) => archiveTaskById(id, opts),
(id) => archiveAndSwitch(id, opts),
setIsArchiving,
"archive",
"sidebar:bulkArchiveFailed",
),
[runBulkRemoval, archiveTaskById, archiveAndSwitch],
);
@@ -102,7 +104,7 @@ function useSidebarBulkActions(
});
},
setIsDeleting,
"delete",
"sidebar:bulkDeleteFailed",
),
[runBulkRemoval, deleteTaskById, removeTaskFromBoard, store],
);
+1
View File
@@ -51,6 +51,7 @@
"delete": "Delete",
"deleteWatchConfirm": "Delete this watch?",
"description": "Description",
"descriptionFallback": "(no description)",
"disable": "Disable",
"disabled": "Disabled",
"discussion": "Discussion",
-2
View File
@@ -25,7 +25,6 @@
"clearAll": "Clear all",
"clearAllQueuedMessages": "Clear all queued messages",
"close": "Close {{name}}",
"closeQuickChat": "Close quick chat",
"collapseClarification": "Collapse clarification",
"collapseMessage": "Collapse message",
"collapseQueuedMessages": "Collapse queued messages",
@@ -99,7 +98,6 @@
"startChatAction": "Start chat",
"starting": "Starting...",
"startingChat": "Starting chat...",
"startNewChat": "Start new chat",
"sendNow": "Send Now",
"sendNowQueuedMessage": "Send queued message now",
"sendNowQueuedMessages": "Send all queued messages now",
+4
View File
@@ -11,6 +11,10 @@
"apiTokens": "API Tokens",
"applicationStatus": "Application status",
"applicationStatusItems": "Application status items",
"bulkArchiveFailed_one": "Failed to archive {{count}} task",
"bulkArchiveFailed_other": "Failed to archive {{count}} tasks",
"bulkDeleteFailed_one": "Failed to delete {{count}} task",
"bulkDeleteFailed_other": "Failed to delete {{count}} tasks",
"closeSettings": "Close settings",
"collapseGroup": "Collapse {{label}}",
"collapseSidebar": "Collapse sidebar",
+13
View File
@@ -348,6 +348,9 @@
"documentSaved": "Document saved",
"documents": "Documents",
"documentsAvailable": "Documents available",
"documentSummary_one": "{{count}} line · {{chars}} chars",
"documentSummary_other": "{{count}} lines · {{chars}} chars",
"documentSummaryEmpty": "empty",
"done": "Done",
"doubleClickToEditOrPress": "Double-click to edit (or press Enter)",
"doubleClickToKeepThisTab": "Double-click to keep this tab open",
@@ -364,6 +367,8 @@
"editBranch2": "Edit branch",
"editComment": "Edit comment",
"editMarkdownComment": "Edit markdown comment",
"editSummaryLines_one": "{{summary}} ({{count}} line)",
"editSummaryLines_other": "{{summary}} ({{count}} lines)",
"editTheMessageForTheMost": "Edit the message for the most recent commit.",
"empty": "(empty)",
"emptyGroup": "Empty group",
@@ -389,6 +394,8 @@
"environmentSetupFinishedWithErrors": "Environment setup finished with errors",
"error": "Error",
"errorData": "Error Data",
"eventCount_one": "{{count}} event",
"eventCount_other": "{{count}} events",
"executor2": "Executor",
"executorEnvironmentIsUnavailable": "Executor environment is unavailable.",
"executorHintDocker": "A Docker container will be created from the selected base branch and checked out on a task branch.",
@@ -490,6 +497,8 @@
"forkANewBranchFromA": "Fork a new branch from a base (turn off to use current checkout)",
"forkANewBranchFromA2": "Fork a new branch from a base instead of using current checkout",
"forkModeANewBranchWill": "Fork mode: a new branch will be created from the selected base before the agent runs. Click to turn off and use your repository's current checkout instead.",
"foundFiles_one": "Found {{count}} file",
"foundFiles_other": "Found {{count}} files",
"freshBranchTooltip": "Create a new branch from the selected base. Any uncommitted changes in your local clone will be discarded; you’ll be asked to confirm if there are any.",
"fromAgent": "From agent",
"fromAgentInTask": "From agent in task <1>“{{fullTitle}}”</1>",
@@ -901,6 +910,9 @@
"ranCommands_one": "· ran {{count}} command",
"ranCommands_other": "· ran {{count}} commands",
"random": "Random",
"read": "Read",
"readLines_one": "Read {{count}} line",
"readLines_other": "Read {{count}} lines",
"reasonAddSessionDescription": "Add a session description",
"reasonAddTaskPrompt": "Add a task prompt",
"reasonAddTaskTitle": "Add a task title",
@@ -1316,6 +1328,7 @@
"toolCount_one": "{{count}} tool",
"toolCount_other": "{{count}} tools",
"tools": "tools",
"toolSearching": "Searching",
"transcribing": "Transcribing…",
"transcribingVoiceInput": "Transcribing voice input",
"treeActionFailed": "Tree action failed",
@@ -51,6 +51,7 @@
"delete": "Ďēĺēţē",
"deleteWatchConfirm": "Ďēĺēţē ţĥĩś ŵàţćĥ?",
"description": "Ďēśćŕĩƥţĩōń",
"descriptionFallback": "(ńō ďēśćŕĩƥţĩōń)",
"disable": "Ďĩśàƀĺē",
"disabled": "Ďĩśàƀĺēď",
"discussion": "Ďĩśćũśśĩōń",
-2
View File
@@ -25,7 +25,6 @@
"clearAll": "Ćĺēàŕ àĺĺ",
"clearAllQueuedMessages": "Ćĺēàŕ àĺĺ qũēũēď ḿēśśàĝēś",
"close": "Ćĺōśē {{name}}",
"closeQuickChat": "Ćĺōśē qũĩćķ ćĥàţ",
"collapseClarification": "Ćōĺĺàƥśē ćĺàŕĩƒĩćàţĩōń",
"collapseMessage": "Ćōĺĺàƥśē ḿēśśàĝē",
"collapseQueuedMessages": "Ćōĺĺàƥśē qũēũēď ḿēśśàĝēś",
@@ -99,7 +98,6 @@
"startChatAction": "Śţàŕţ ćĥàţ",
"starting": "Śţàŕţĩńĝ...",
"startingChat": "Śţàŕţĩńĝ ćĥàţ...",
"startNewChat": "Śţàŕţ ńēŵ ćĥàţ",
"sendNow": "Śēńď Ńōŵ",
"sendNowQueuedMessage": "Śēńď qũēũēď ḿēśśàĝē ńōŵ",
"sendNowQueuedMessages": "Śēńď àĺĺ qũēũēď ḿēśśàĝēś ńōŵ",
+4
View File
@@ -11,6 +11,10 @@
"apiTokens": "ÀƤĨ Ţōķēńś",
"applicationStatus": "Àƥƥĺĩćàţĩōń śţàţũś",
"applicationStatusItems": "Àƥƥĺĩćàţĩōń śţàţũś ĩţēḿś",
"bulkArchiveFailed_one": "Ƒàĩĺēď ţō àŕćĥĩvē {{count}} ţàśķ",
"bulkArchiveFailed_other": "Ƒàĩĺēď ţō àŕćĥĩvē {{count}} ţàśķś",
"bulkDeleteFailed_one": "Ƒàĩĺēď ţō ďēĺēţē {{count}} ţàśķ",
"bulkDeleteFailed_other": "Ƒàĩĺēď ţō ďēĺēţē {{count}} ţàśķś",
"closeSettings": "Ćĺōśē śēţţĩńĝś",
"collapseGroup": "Ćōĺĺàƥśē {{label}}",
"collapseSidebar": "Ćōĺĺàƥśē śĩďēƀàŕ",
+13
View File
@@ -348,6 +348,9 @@
"documentSaved": "Ďōćũḿēńţ śàvēď",
"documents": "Ďōćũḿēńţś",
"documentsAvailable": "Ďōćũḿēńţś àvàĩĺàƀĺē",
"documentSummary_one": "{{count}} ĺĩńē · {{chars}} ćĥàŕś",
"documentSummary_other": "{{count}} ĺĩńēś · {{chars}} ćĥàŕś",
"documentSummaryEmpty": "ēḿƥţŷ",
"done": "Ďōńē",
"doubleClickToEditOrPress": "Ďōũƀĺē-ćĺĩćķ ţō ēďĩţ (ōŕ ƥŕēśś Ēńţēŕ)",
"doubleClickToKeepThisTab": "Ďōũƀĺē-ćĺĩćķ ţō ķēēƥ ţĥĩś ţàƀ ōƥēń",
@@ -364,6 +367,8 @@
"editBranch2": "Ēďĩţ ƀŕàńćĥ",
"editComment": "Ēďĩţ ćōḿḿēńţ",
"editMarkdownComment": "Ēďĩţ ḿàŕķďōŵń ćōḿḿēńţ",
"editSummaryLines_one": "{{summary}} ({{count}} ĺĩńē)",
"editSummaryLines_other": "{{summary}} ({{count}} ĺĩńēś)",
"editTheMessageForTheMost": "Ēďĩţ ţĥē ḿēśśàĝē ƒōŕ ţĥē ḿōśţ ŕēćēńţ ćōḿḿĩţ.",
"empty": "(ēḿƥţŷ)",
"emptyGroup": "Ēḿƥţŷ ĝŕōũƥ",
@@ -389,6 +394,8 @@
"environmentSetupFinishedWithErrors": "Ēńvĩŕōńḿēńţ śēţũƥ ƒĩńĩśĥēď ŵĩţĥ ēŕŕōŕś",
"error": "Ēŕŕōŕ",
"errorData": "Ēŕŕōŕ Ďàţà",
"eventCount_one": "{{count}} ēvēńţ",
"eventCount_other": "{{count}} ēvēńţś",
"executor2": "Ēxēćũţōŕ",
"executorEnvironmentIsUnavailable": "Ēxēćũţōŕ ēńvĩŕōńḿēńţ ĩś ũńàvàĩĺàƀĺē.",
"executorHintDocker": "À Ďōćķēŕ ćōńţàĩńēŕ ŵĩĺĺ ƀē ćŕēàţēď ƒŕōḿ ţĥē śēĺēćţēď ƀàśē ƀŕàńćĥ àńď ćĥēćķēď ōũţ ōń à ţàśķ ƀŕàńćĥ.",
@@ -490,6 +497,8 @@
"forkANewBranchFromA": "Ƒōŕķ à ńēŵ ƀŕàńćĥ ƒŕōḿ à ƀàśē (ţũŕń ōƒƒ ţō ũśē ćũŕŕēńţ ćĥēćķōũţ)",
"forkANewBranchFromA2": "Ƒōŕķ à ńēŵ ƀŕàńćĥ ƒŕōḿ à ƀàśē ĩńśţēàď ōƒ ũśĩńĝ ćũŕŕēńţ ćĥēćķōũţ",
"forkModeANewBranchWill": "Ƒōŕķ ḿōďē: à ńēŵ ƀŕàńćĥ ŵĩĺĺ ƀē ćŕēàţēď ƒŕōḿ ţĥē śēĺēćţēď ƀàśē ƀēƒōŕē ţĥē àĝēńţ ŕũńś. Ćĺĩćķ ţō ţũŕń ōƒƒ àńď ũśē ŷōũŕ ŕēƥōśĩţōŕŷ'ś ćũŕŕēńţ ćĥēćķōũţ ĩńśţēàď.",
"foundFiles_one": "Ƒōũńď {{count}} ƒĩĺē",
"foundFiles_other": "Ƒōũńď {{count}} ƒĩĺēś",
"freshBranchTooltip": "Ćŕēàţē à ńēŵ ƀŕàńćĥ ƒŕōḿ ţĥē śēĺēćţēď ƀàśē. Àńŷ ũńćōḿḿĩţţēď ćĥàńĝēś ĩń ŷōũŕ ĺōćàĺ ćĺōńē ŵĩĺĺ ƀē ďĩśćàŕďēď; ŷōũ’ĺĺ ƀē àśķēď ţō ćōńƒĩŕḿ ĩƒ ţĥēŕē àŕē àńŷ.",
"fromAgent": "Ƒŕōḿ àĝēńţ",
"fromAgentInTask": "Ƒŕōḿ àĝēńţ ĩń ţàśķ <1>“{{fullTitle}}”</1>",
@@ -901,6 +910,9 @@
"ranCommands_one": "· ŕàń {{count}} ćōḿḿàńď",
"ranCommands_other": "· ŕàń {{count}} ćōḿḿàńďś",
"random": "Ŕàńďōḿ",
"read": "Ŕēàď",
"readLines_one": "Ŕēàď {{count}} ĺĩńē",
"readLines_other": "Ŕēàď {{count}} ĺĩńēś",
"reasonAddSessionDescription": "Àďď à śēśśĩōń ďēśćŕĩƥţĩōń",
"reasonAddTaskPrompt": "Àďď à ţàśķ ƥŕōḿƥţ",
"reasonAddTaskTitle": "Àďď à ţàśķ ţĩţĺē",
@@ -1316,6 +1328,7 @@
"toolCount_one": "{{count}} ţōōĺ",
"toolCount_other": "{{count}} ţōōĺś",
"tools": "ţōōĺś",
"toolSearching": "Śēàŕćĥĩńĝ",
"transcribing": "Ţŕàńśćŕĩƀĩńĝ…",
"transcribingVoiceInput": "Ţŕàńśćŕĩƀĩńĝ vōĩćē ĩńƥũţ",
"treeActionFailed": "Ţŕēē àćţĩōń ƒàĩĺēď",