feat(i18n): translate modals namespace

Translates all 11 modal sources: registry (no UI text), backlog-agent-hint,
set-parent-issue, add-child-issue, delete-issue-confirm, feedback,
issue-picker, create-workspace, create-project, create-issue (manual),
quick-create-issue (agent panel).

Notable patterns:
- create-project re-uses useProjectStatusLabels / useProjectPriorityLabels
  hooks from views/projects/components/labels — same translation source
  as the projects list / detail, no duplication.
- create-issue.tsx: renamed `toast.custom((t) => ...)` callback param to
  `toastId` to avoid shadowing the closure-captured useT() `t` function.
- Test wrapper added to modals/create-issue.test.tsx so the two assertions
  on rendered modal text (success toast + Create another) match the EN
  bundle. modals/create-workspace.test.tsx was already wrapped (workspace
  ns commit).

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-04-30 14:57:36 +08:00
parent 59c1852db1
commit 33aaa7c36e
16 changed files with 445 additions and 111 deletions

View File

@@ -59,6 +59,16 @@ const TRANSLATED_FILES = [
"chat/components/no-agent-banner.tsx",
"chat/components/offline-banner.tsx",
"chat/components/task-status-pill.tsx",
"modals/backlog-agent-hint.tsx",
"modals/set-parent-issue.tsx",
"modals/add-child-issue.tsx",
"modals/delete-issue-confirm.tsx",
"modals/feedback.tsx",
"modals/issue-picker-modal.tsx",
"modals/create-workspace.tsx",
"modals/create-project.tsx",
"modals/create-issue.tsx",
"modals/quick-create-issue.tsx",
];
export default [

View File

@@ -17,6 +17,7 @@ import type projects from "../locales/en/projects.json";
import type autopilots from "../locales/en/autopilots.json";
import type skills from "../locales/en/skills.json";
import type chat from "../locales/en/chat.json";
import type modals from "../locales/en/modals.json";
// Module augmentation enables i18next v26 selector API across the monorepo:
// `t($ => $.signin.title)` resolves to the value in en/auth.json.
@@ -48,6 +49,7 @@ declare module "i18next" {
autopilots: typeof autopilots;
skills: typeof skills;
chat: typeof chat;
modals: typeof modals;
};
enableSelector: true;
}

View File

@@ -0,0 +1,128 @@
{
"common": {
"back": "Back",
"close": "Close",
"cancel": "Cancel",
"expand_tooltip": "Expand",
"collapse_tooltip": "Collapse"
},
"create_workspace": {
"title": "Create a new workspace",
"description": "Workspaces are shared environments where teams can work on projects and issues."
},
"delete_issue": {
"title": "Delete issue",
"description": "This will permanently delete this issue and all its comments. This action cannot be undone.",
"hint": "Any workspace member can delete issues.",
"cancel": "Cancel",
"confirm": "Delete",
"deleting": "Deleting...",
"toast_deleted": "Issue deleted",
"toast_delete_failed": "Failed to delete issue"
},
"feedback": {
"title": "Feedback",
"description": "We'd love to hear what's working, what isn't, or what you'd like to see next.",
"placeholder": "Tell us about your experience, bugs you've found, or features you'd like to see…",
"toast_uploading": "Please wait for uploads to finish…",
"toast_too_long": "Message is too long",
"toast_sent": "Thanks for the feedback!",
"toast_failed": "Failed to send feedback",
"send": "Send feedback",
"sending": "Sending…"
},
"issue_picker": {
"search_placeholder": "Search issues...",
"searching": "Searching...",
"no_results": "No issues found.",
"prompt_to_search": "Type to search issues"
},
"set_parent": {
"title": "Set parent issue",
"description": "Search for an issue to set as the parent of this issue",
"toast_failed": "Failed to update issue",
"toast_success": "Set {{identifier}} as parent issue"
},
"add_child": {
"title": "Add sub-issue",
"description": "Search for an issue to add as a sub-issue",
"toast_failed": "Failed to add sub-issue",
"toast_success": "Added {{identifier}} as sub-issue"
},
"backlog_hint": {
"toast_status_failed": "Failed to update status"
},
"create_project": {
"title": "New Project",
"title_breadcrumb": "New project",
"icon_tooltip": "Choose icon",
"title_placeholder": "Project title",
"description_placeholder": "Add description...",
"lead": "Lead",
"no_lead": "No lead",
"lead_placeholder": "Assign lead...",
"members_group": "Members",
"agents_group": "Agents",
"no_results": "No results",
"submit": "Create Project",
"submitting": "Creating...",
"toast_created": "Project created",
"toast_failed": "Failed to create project"
},
"create_issue": {
"sr_manual": "New Issue",
"sr_agent": "Quick create issue",
"manual_breadcrumb": "Create manually",
"agent_breadcrumb": "Create with agent",
"title_placeholder": "Issue title",
"description_placeholder": "Add description...",
"more_options_aria": "More options",
"submit": "Create Issue",
"submitting": "Creating...",
"toast_created": "Issue created",
"view_issue": "View issue",
"toast_failed": "Failed to create issue",
"toast_link_subissues_all_failed": "Failed to link sub-issues",
"toast_link_subissues_partial": "Failed to link {{failed}} of {{total}} sub-issues",
"switch_to_agent": "Switch to Agent",
"switch_to_agent_tooltip": "Switch to create with agent — describe in one line and let the agent file it",
"switch_to_manual": "Switch to Manual",
"switch_to_manual_tooltip": "Switch to manual create — fill the fields yourself",
"create_another": "Create another",
"remove_parent_aria": "Remove parent",
"remove_subissue_aria": "Remove sub-issue {{identifier}}",
"subissue_of": "Sub-issue of {{identifier}}",
"subissue_chip": "Sub-issue: {{identifier}}",
"parent_with_id": "Parent: {{identifier}}",
"set_parent": "Set parent issue...",
"add_subissue": "Add sub-issue...",
"remove_parent": "Remove parent",
"set_parent_picker": {
"title": "Set parent issue",
"description": "Search for an issue to set as the parent of the new issue"
},
"add_subissue_picker": {
"title": "Add sub-issue",
"description": "Search for an issue to add as a sub-issue of the new issue"
},
"agent": {
"created_by": "Created by",
"select_agent_aria": "Select agent",
"pick_an_agent": "Pick an agent…",
"no_agents": "No agents available.",
"version_missing": "This agent's daemon doesn't report a CLI version. Create with agent needs multica CLI ≥ {{min}}. Upgrade the daemon and reconnect, or switch to manual create.",
"version_below": "This agent's daemon CLI is {{current}} — Create with agent needs ≥ {{min}}. Upgrade the daemon, or switch to manual create.",
"prompt_placeholder": "Tell the agent what to do, e.g. \"let Bohan fix the inbox loading slowness in the Web project\"",
"submit": "Create (⌘↵)",
"sending": "Sending…",
"uploading": "Uploading…",
"sent_label": "Sent",
"sent_count": "{{count}} sent",
"version_blocked_tooltip": "Daemon CLI must be ≥ {{min}}",
"toast_sent": "Sent to agent — you'll get an inbox notification when it's done",
"error_agent_unavailable_fallback": "Agent is unavailable. Pick another agent.",
"error_daemon_version": "This agent's daemon CLI ({{current}}) is below the required {{min}}. Upgrade the daemon to use Create with agent.",
"error_unknown": "Failed to submit. Try again."
}
}
}

View File

@@ -14,6 +14,7 @@ import enProjects from "./en/projects.json";
import enAutopilots from "./en/autopilots.json";
import enSkills from "./en/skills.json";
import enChat from "./en/chat.json";
import enModals from "./en/modals.json";
import zhHansCommon from "./zh-Hans/common.json";
import zhHansAuth from "./zh-Hans/auth.json";
import zhHansSettings from "./zh-Hans/settings.json";
@@ -29,6 +30,7 @@ import zhHansProjects from "./zh-Hans/projects.json";
import zhHansAutopilots from "./zh-Hans/autopilots.json";
import zhHansSkills from "./zh-Hans/skills.json";
import zhHansChat from "./zh-Hans/chat.json";
import zhHansModals from "./zh-Hans/modals.json";
// Single source of truth for the resource bundle. Both apps (web layout +
// desktop App.tsx) import from here so adding a locale or namespace happens
@@ -50,6 +52,7 @@ export const RESOURCES: Record<SupportedLocale, LocaleResources> = {
autopilots: enAutopilots,
skills: enSkills,
chat: enChat,
modals: enModals,
},
"zh-Hans": {
common: zhHansCommon,
@@ -67,5 +70,6 @@ export const RESOURCES: Record<SupportedLocale, LocaleResources> = {
autopilots: zhHansAutopilots,
skills: zhHansSkills,
chat: zhHansChat,
modals: zhHansModals,
},
};

View File

@@ -0,0 +1,128 @@
{
"common": {
"back": "返回",
"close": "关闭",
"cancel": "取消",
"expand_tooltip": "展开",
"collapse_tooltip": "收起"
},
"create_workspace": {
"title": "创建新工作区",
"description": "工作区是团队协作处理 project 和 issue 的共享空间。"
},
"delete_issue": {
"title": "删除 issue",
"description": "将永久删除该 issue 及其所有评论。此操作不可撤销。",
"hint": "工作区任何成员都可以删除 issue。",
"cancel": "取消",
"confirm": "删除",
"deleting": "删除中...",
"toast_deleted": "已删除 issue",
"toast_delete_failed": "删除 issue 失败"
},
"feedback": {
"title": "反馈",
"description": "告诉我们哪些好用、哪些不好用、或者你希望我们做什么。",
"placeholder": "聊聊你的体验、遇到的 bug或想看到的功能…",
"toast_uploading": "请等待上传完成…",
"toast_too_long": "内容过长",
"toast_sent": "感谢反馈!",
"toast_failed": "发送反馈失败",
"send": "发送反馈",
"sending": "发送中…"
},
"issue_picker": {
"search_placeholder": "搜索 issue...",
"searching": "搜索中...",
"no_results": "未找到 issue。",
"prompt_to_search": "输入关键词搜索 issue"
},
"set_parent": {
"title": "设置父 issue",
"description": "搜索一个 issue将其设为当前 issue 的父级",
"toast_failed": "更新 issue 失败",
"toast_success": "已将 {{identifier}} 设为父 issue"
},
"add_child": {
"title": "添加子 issue",
"description": "搜索一个 issue 添加为子 issue",
"toast_failed": "添加子 issue 失败",
"toast_success": "已添加 {{identifier}} 为子 issue"
},
"backlog_hint": {
"toast_status_failed": "更新状态失败"
},
"create_project": {
"title": "新建 project",
"title_breadcrumb": "新建 project",
"icon_tooltip": "选择图标",
"title_placeholder": "project 标题",
"description_placeholder": "添加描述...",
"lead": "负责人",
"no_lead": "无负责人",
"lead_placeholder": "指派负责人...",
"members_group": "成员",
"agents_group": "智能体",
"no_results": "未找到结果",
"submit": "创建 project",
"submitting": "创建中...",
"toast_created": "已创建 project",
"toast_failed": "创建 project 失败"
},
"create_issue": {
"sr_manual": "新建 issue",
"sr_agent": "快速创建 issue",
"manual_breadcrumb": "手动创建",
"agent_breadcrumb": "通过智能体创建",
"title_placeholder": "issue 标题",
"description_placeholder": "添加描述...",
"more_options_aria": "更多选项",
"submit": "创建 issue",
"submitting": "创建中...",
"toast_created": "已创建 issue",
"view_issue": "查看 issue",
"toast_failed": "创建 issue 失败",
"toast_link_subissues_all_failed": "关联子 issue 失败",
"toast_link_subissues_partial": "{{total}} 个子 issue 中有 {{failed}} 个关联失败",
"switch_to_agent": "切换到智能体",
"switch_to_agent_tooltip": "切换到通过智能体创建——一句话描述,让它替你建",
"switch_to_manual": "切换到手动",
"switch_to_manual_tooltip": "切换到手动创建——自己填字段",
"create_another": "继续创建",
"remove_parent_aria": "移除父级",
"remove_subissue_aria": "移除子 issue {{identifier}}",
"subissue_of": "{{identifier}} 的子 issue",
"subissue_chip": "子 issue{{identifier}}",
"parent_with_id": "父:{{identifier}}",
"set_parent": "设置父 issue...",
"add_subissue": "添加子 issue...",
"remove_parent": "移除父级",
"set_parent_picker": {
"title": "设置父 issue",
"description": "搜索一个 issue作为新 issue 的父级"
},
"add_subissue_picker": {
"title": "添加子 issue",
"description": "搜索一个 issue作为新 issue 的子级"
},
"agent": {
"created_by": "创建者",
"select_agent_aria": "选择智能体",
"pick_an_agent": "选一个智能体…",
"no_agents": "暂无可用智能体。",
"version_missing": "该智能体的 daemon 没有报告 CLI 版本。通过智能体创建需要 multica CLI ≥ {{min}}。请升级 daemon 并重连,或切换到手动创建。",
"version_below": "该智能体的 daemon CLI 是 {{current}}——通过智能体创建需要 ≥ {{min}}。请升级 daemon或切换到手动创建。",
"prompt_placeholder": "告诉智能体要做什么,例如:\"让 Bohan 修一下 Web 项目里收件箱加载慢的问题\"",
"submit": "创建(⌘↵)",
"sending": "发送中…",
"uploading": "上传中…",
"sent_label": "已发送",
"sent_count": "已发送 {{count}}",
"version_blocked_tooltip": "daemon CLI 必须 ≥ {{min}}",
"toast_sent": "已交给智能体——完成后会在收件箱通知你",
"error_agent_unavailable_fallback": "智能体不可用,请换一个。",
"error_daemon_version": "该智能体的 daemon CLI{{current}})低于要求的 {{min}}。请升级 daemon 后再使用通过智能体创建。",
"error_unknown": "提交失败,请重试。"
}
}
}

View File

@@ -9,6 +9,7 @@ import {
} from "@multica/core/issues/queries";
import { useUpdateIssue } from "@multica/core/issues/mutations";
import { IssuePickerModal } from "./issue-picker-modal";
import { useT } from "../i18n";
export function AddChildIssueModal({
onClose,
@@ -17,6 +18,7 @@ export function AddChildIssueModal({
onClose: () => void;
data: Record<string, unknown> | null;
}) {
const { t } = useT("modals");
const issueId = (data?.issueId as string) || "";
const wsId = useWorkspaceId();
const updateIssue = useUpdateIssue();
@@ -42,15 +44,15 @@ export function AddChildIssueModal({
onOpenChange={(v) => {
if (!v) onClose();
}}
title="Add sub-issue"
description="Search for an issue to add as a sub-issue"
title={t(($) => $.add_child.title)}
description={t(($) => $.add_child.description)}
excludeIds={excludeIds}
onSelect={(selected) => {
updateIssue.mutate(
{ id: selected.id, parent_issue_id: issueId },
{ onError: () => toast.error("Failed to add sub-issue") },
{ onError: () => toast.error(t(($) => $.add_child.toast_failed)) },
);
toast.success(`Added ${selected.identifier} as sub-issue`);
toast.success(t(($) => $.add_child.toast_success, { identifier: selected.identifier }));
}}
/>
);

View File

@@ -3,6 +3,7 @@
import { toast } from "sonner";
import { BacklogAgentHintDialog } from "../issues/components/backlog-agent-hint-dialog";
import { useUpdateIssue } from "@multica/core/issues/mutations";
import { useT } from "../i18n";
export function BacklogAgentHintModal({
onClose,
@@ -11,6 +12,7 @@ export function BacklogAgentHintModal({
onClose: () => void;
data: Record<string, unknown> | null;
}) {
const { t } = useT("modals");
const issueId = (data?.issueId as string) || "";
const updateIssue = useUpdateIssue();
@@ -27,7 +29,7 @@ export function BacklogAgentHintModal({
if (issueId) {
updateIssue.mutate(
{ id: issueId, status: "todo" },
{ onError: () => toast.error("Failed to update status") },
{ onError: () => toast.error(t(($) => $.backlog_hint.toast_status_failed)) },
);
}
onClose();

View File

@@ -1,8 +1,23 @@
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
import { forwardRef, useImperativeHandle, useRef, useState, type ReactNode } from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@multica/core/i18n/react";
import enCommon from "../locales/en/common.json";
import enModals from "../locales/en/modals.json";
const TEST_RESOURCES = {
en: { common: enCommon, modals: enModals },
};
function I18nWrapper({ children }: { children: ReactNode }) {
return (
<I18nProvider locale="en" resources={TEST_RESOURCES}>
{children}
</I18nProvider>
);
}
const mockPush = vi.hoisted(() => vi.fn());
const mockCreateIssue = vi.hoisted(() => vi.fn());
@@ -236,7 +251,11 @@ function renderModal(element: React.ReactElement) {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(<QueryClientProvider client={qc}>{element}</QueryClientProvider>);
return render(
<I18nWrapper>
<QueryClientProvider client={qc}>{element}</QueryClientProvider>
</I18nWrapper>,
);
}
describe("CreateIssueModal", () => {

View File

@@ -47,6 +47,7 @@ import { api } from "@multica/core/api";
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
import { PillButton } from "../common/pill-button";
import { IssuePickerModal } from "./issue-picker-modal";
import { useT } from "../i18n";
// ---------------------------------------------------------------------------
// ManualCreatePanel — manual-mode body of the create-issue dialog. Renders
@@ -77,6 +78,7 @@ export function ManualCreatePanel({
backlogHintIssueId: string | null;
setBacklogHintIssueId: (id: string | null) => void;
}) {
const { t } = useT("modals");
const router = useNavigation();
const p = useWorkspacePaths();
const workspaceName = useCurrentWorkspace()?.name;
@@ -198,8 +200,11 @@ export function ManualCreatePanel({
if (failed > 0) {
toast.error(
failed === childIssues.length
? "Failed to link sub-issues"
: `Failed to link ${failed} of ${childIssues.length} sub-issues`,
? t(($) => $.create_issue.toast_link_subissues_all_failed)
: t(($) => $.create_issue.toast_link_subissues_partial, {
failed,
total: childIssues.length,
}),
);
}
}
@@ -220,13 +225,13 @@ export function ManualCreatePanel({
}
if (!shouldShowBacklogHint) {
toast.custom((t) => (
toast.custom((toastId) => (
<div className="bg-popover text-popover-foreground border rounded-lg shadow-lg p-4 w-[360px]">
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center justify-center size-5 rounded-full bg-emerald-500/15 text-emerald-500">
<Check className="size-3" />
</div>
<span className="text-sm font-medium">Issue created</span>
<span className="text-sm font-medium">{t(($) => $.create_issue.toast_created)}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground ml-7">
<StatusIcon status={issue.status} className="size-3.5 shrink-0" />
@@ -237,16 +242,16 @@ export function ManualCreatePanel({
className="ml-7 mt-2 text-sm text-primary hover:underline cursor-pointer"
onClick={() => {
router.push(p.issueDetail(issue.id));
toast.dismiss(t);
toast.dismiss(toastId);
}}
>
View issue
{t(($) => $.create_issue.view_issue)}
</button>
</div>
), { duration: 5000 });
}
} catch {
toast.error("Failed to create issue");
toast.error(t(($) => $.create_issue.toast_failed));
} finally {
setSubmitting(false);
}
@@ -283,7 +288,7 @@ export function ManualCreatePanel({
onMoveToTodo={() => {
updateIssueMutation.mutate(
{ id: backlogHintIssueId, status: "todo" },
{ onError: () => toast.error("Failed to update status") },
{ onError: () => toast.error(t(($) => $.backlog_hint.toast_status_failed)) },
);
setBacklogHintIssueId(null);
onClose();
@@ -291,14 +296,14 @@ export function ManualCreatePanel({
/>
) : (
<>
<DialogTitle className="sr-only">New Issue</DialogTitle>
<DialogTitle className="sr-only">{t(($) => $.create_issue.sr_manual)}</DialogTitle>
{/* Header */}
<div className="flex items-center justify-between px-5 pt-3 pb-2 shrink-0">
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">{workspaceName}</span>
<ChevronRight className="size-3 text-muted-foreground/50" />
<span className="font-medium">Create manually</span>
<span className="font-medium">{t(($) => $.create_issue.manual_breadcrumb)}</span>
</div>
<div className="flex items-center gap-1">
<Tooltip>
@@ -312,7 +317,11 @@ export function ManualCreatePanel({
</button>
}
/>
<TooltipContent side="bottom">{isExpanded ? "Collapse" : "Expand"}</TooltipContent>
<TooltipContent side="bottom">
{isExpanded
? t(($) => $.common.collapse_tooltip)
: t(($) => $.common.expand_tooltip)}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
@@ -325,7 +334,7 @@ export function ManualCreatePanel({
</button>
}
/>
<TooltipContent side="bottom">Close</TooltipContent>
<TooltipContent side="bottom">{t(($) => $.common.close)}</TooltipContent>
</Tooltip>
</div>
</div>
@@ -336,7 +345,7 @@ export function ManualCreatePanel({
key={formResetKey}
autoFocus
defaultValue={draft.title}
placeholder="Issue title"
placeholder={t(($) => $.create_issue.title_placeholder)}
className="text-lg font-semibold"
onChange={(v) => updateTitle(v)}
onSubmit={handleSubmit}
@@ -348,7 +357,7 @@ export function ManualCreatePanel({
<ContentEditor
ref={descEditorRef}
defaultValue={draft.description}
placeholder="Add description..."
placeholder={t(($) => $.create_issue.description_placeholder)}
onUpdate={(md) => setDraft({ description: md })}
onUploadFile={handleUpload}
debounceMs={500}
@@ -413,13 +422,15 @@ export function ManualCreatePanel({
className="flex items-center gap-1.5 py-1 pl-2.5 cursor-pointer"
>
<ArrowUp className="size-3 text-muted-foreground" />
<span>Sub-issue of {parentIssue.identifier}</span>
<span>
{t(($) => $.create_issue.subissue_of, { identifier: parentIssue.identifier })}
</span>
</button>
<button
type="button"
onClick={() => setParentIssueId(undefined)}
className="p-1 pr-2 text-muted-foreground hover:text-foreground cursor-pointer"
aria-label="Remove parent"
aria-label={t(($) => $.create_issue.remove_parent_aria)}
>
<XIcon className="size-3" />
</button>
@@ -435,7 +446,7 @@ export function ManualCreatePanel({
>
<div className="flex items-center gap-1.5 py-1 pl-2.5">
<ArrowDown className="size-3 text-muted-foreground" />
<span>Sub-issue: {c.identifier}</span>
<span>{t(($) => $.create_issue.subissue_chip, { identifier: c.identifier })}</span>
</div>
<button
type="button"
@@ -443,7 +454,7 @@ export function ManualCreatePanel({
setChildIssues((prev) => prev.filter((x) => x.id !== c.id))
}
className="p-1 pr-2 text-muted-foreground hover:text-foreground cursor-pointer"
aria-label={`Remove sub-issue ${c.identifier}`}
aria-label={t(($) => $.create_issue.remove_subissue_aria, { identifier: c.identifier })}
>
<XIcon className="size-3" />
</button>
@@ -455,7 +466,7 @@ export function ManualCreatePanel({
<DropdownMenu>
<DropdownMenuTrigger
render={
<PillButton aria-label="More options">
<PillButton aria-label={t(($) => $.create_issue.more_options_aria)}>
<MoreHorizontal className="size-3.5" />
</PillButton>
}
@@ -464,17 +475,17 @@ export function ManualCreatePanel({
{parentIssueId && parentIssue ? (
<DropdownMenuItem onClick={() => setParentPickerOpen(true)}>
<ArrowUp className="h-3.5 w-3.5" />
Parent: {parentIssue.identifier}
{t(($) => $.create_issue.parent_with_id, { identifier: parentIssue.identifier })}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => setParentPickerOpen(true)}>
<ArrowUp className="h-3.5 w-3.5" />
Set parent issue...
{t(($) => $.create_issue.set_parent)}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setChildPickerOpen(true)}>
<ArrowDown className="h-3.5 w-3.5" />
Add sub-issue...
{t(($) => $.create_issue.add_subissue)}
</DropdownMenuItem>
{parentIssueId && parentIssue && (
<>
@@ -484,7 +495,7 @@ export function ManualCreatePanel({
onClick={() => setParentIssueId(undefined)}
>
<XIcon className="h-3.5 w-3.5" />
Remove parent
{t(($) => $.create_issue.remove_parent)}
</DropdownMenuItem>
</>
)}
@@ -497,8 +508,8 @@ export function ManualCreatePanel({
<IssuePickerModal
open={parentPickerOpen}
onOpenChange={setParentPickerOpen}
title="Set parent issue"
description="Search for an issue to set as the parent of the new issue"
title={t(($) => $.create_issue.set_parent_picker.title)}
description={t(($) => $.create_issue.set_parent_picker.description)}
excludeIds={[
...childIssues.map((c) => c.id),
...(parentIssueId ? [parentIssueId] : []),
@@ -510,8 +521,8 @@ export function ManualCreatePanel({
<IssuePickerModal
open={childPickerOpen}
onOpenChange={setChildPickerOpen}
title="Add sub-issue"
description="Search for an issue to add as a sub-issue of the new issue"
title={t(($) => $.create_issue.add_subissue_picker.title)}
description={t(($) => $.create_issue.add_subissue_picker.description)}
excludeIds={[
...childIssues.map((c) => c.id),
...(parentIssueId ? [parentIssueId] : []),
@@ -534,11 +545,11 @@ export function ManualCreatePanel({
<button
type="button"
onClick={switchToAgent}
title="Switch to create with agent — describe in one line and let the agent file it"
title={t(($) => $.create_issue.switch_to_agent_tooltip)}
className="flex shrink-0 items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Switch to Agent
{t(($) => $.create_issue.switch_to_agent)}
</button>
<label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Switch
@@ -546,10 +557,10 @@ export function ManualCreatePanel({
checked={keepOpen}
onCheckedChange={setKeepOpen}
/>
Create another
{t(($) => $.create_issue.create_another)}
</label>
<Button size="sm" onClick={handleSubmit} disabled={!title.trim() || submitting}>
{submitting ? "Creating..." : "Create Issue"}
{submitting ? t(($) => $.create_issue.submitting) : t(($) => $.create_issue.submit)}
</Button>
</div>
</div>

View File

@@ -8,7 +8,6 @@ import { useProjectDraftStore } from "@multica/core/projects";
import {
PROJECT_STATUS_CONFIG,
PROJECT_STATUS_ORDER,
PROJECT_PRIORITY_CONFIG,
PROJECT_PRIORITY_ORDER,
} from "@multica/core/projects/config";
import { useWorkspaceId } from "@multica/core/hooks";
@@ -33,6 +32,11 @@ import { ContentEditor, type ContentEditorRef, TitleEditor } from "../editor";
import { PriorityIcon } from "../issues/components/priority-icon";
import { ActorAvatar } from "../common/actor-avatar";
import { useNavigation } from "../navigation";
import { useT } from "../i18n";
import {
useProjectStatusLabels,
useProjectPriorityLabels,
} from "../projects/components/labels";
function PillButton({
children,
@@ -55,6 +59,7 @@ function PillButton({
}
export function CreateProjectModal({ onClose }: { onClose: () => void }) {
const { t } = useT("modals");
const router = useNavigation();
const workspace = useCurrentWorkspace();
const workspaceName = workspace?.name;
@@ -63,6 +68,8 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
const { data: members = [] } = useQuery(memberListOptions(wsId));
const { data: agents = [] } = useQuery(agentListOptions(wsId));
const { getActorName } = useActorName();
const projectStatusLabels = useProjectStatusLabels();
const projectPriorityLabels = useProjectPriorityLabels();
const draft = useProjectDraftStore((s) => s.draft);
const setDraft = useProjectDraftStore((s) => s.setDraft);
@@ -98,7 +105,8 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
(a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery),
);
const leadLabel = leadType && leadId ? getActorName(leadType, leadId) : "Lead";
const leadLabel =
leadType && leadId ? getActorName(leadType, leadId) : t(($) => $.create_project.lead);
const createProject = useCreateProject();
@@ -117,10 +125,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
});
clearDraft();
onClose();
toast.success("Project created");
toast.success(t(($) => $.create_project.toast_created));
router.push(wsPaths.projectDetail(project.id));
} catch {
toast.error("Failed to create project");
toast.error(t(($) => $.create_project.toast_failed));
} finally {
setSubmitting(false);
}
@@ -139,13 +147,13 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
: "!max-w-2xl !w-full !h-96 !-translate-y-1/2",
)}
>
<DialogTitle className="sr-only">New Project</DialogTitle>
<DialogTitle className="sr-only">{t(($) => $.create_project.title)}</DialogTitle>
<div className="flex items-center justify-between px-5 pt-3 pb-2 shrink-0">
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">{workspaceName}</span>
<ChevronRight className="size-3 text-muted-foreground/50" />
<span className="font-medium">New project</span>
<span className="font-medium">{t(($) => $.create_project.title_breadcrumb)}</span>
</div>
<div className="flex items-center gap-1">
<Tooltip>
@@ -159,7 +167,11 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
</button>
}
/>
<TooltipContent side="bottom">{isExpanded ? "Collapse" : "Expand"}</TooltipContent>
<TooltipContent side="bottom">
{isExpanded
? t(($) => $.common.collapse_tooltip)
: t(($) => $.common.expand_tooltip)}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
@@ -172,7 +184,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
</button>
}
/>
<TooltipContent side="bottom">Close</TooltipContent>
<TooltipContent side="bottom">{t(($) => $.common.close)}</TooltipContent>
</Tooltip>
</div>
</div>
@@ -184,7 +196,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
<button
type="button"
className="text-2xl cursor-pointer rounded-lg p-1 -ml-1 hover:bg-accent/60 transition-colors"
title="Choose icon"
title={t(($) => $.create_project.icon_tooltip)}
>
{icon || "📁"}
</button>
@@ -202,7 +214,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
<TitleEditor
autoFocus
defaultValue={draft.title}
placeholder="Project title"
placeholder={t(($) => $.create_project.title_placeholder)}
className="text-lg font-semibold"
onChange={(v) => updateTitle(v)}
onSubmit={handleSubmit}
@@ -213,7 +225,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
<ContentEditor
ref={descEditorRef}
defaultValue={draft.description}
placeholder="Add description..."
placeholder={t(($) => $.create_project.description_placeholder)}
onUpdate={(md) => setDraft({ description: md })}
debounceMs={500}
/>
@@ -225,7 +237,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
render={
<PillButton>
<span className={cn("size-2 rounded-full", PROJECT_STATUS_CONFIG[status].dotColor)} />
<span>{PROJECT_STATUS_CONFIG[status].label}</span>
<span>{projectStatusLabels[status]}</span>
</PillButton>
}
/>
@@ -233,7 +245,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
{PROJECT_STATUS_ORDER.map((s) => (
<DropdownMenuItem key={s} onClick={() => updateStatus(s)}>
<span className={cn("size-2 rounded-full", PROJECT_STATUS_CONFIG[s].dotColor)} />
<span>{PROJECT_STATUS_CONFIG[s].label}</span>
<span>{projectStatusLabels[s]}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -244,7 +256,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
render={
<PillButton>
<PriorityIcon priority={priority} />
<span>{PROJECT_PRIORITY_CONFIG[priority].label}</span>
<span>{projectPriorityLabels[priority]}</span>
</PillButton>
}
/>
@@ -252,7 +264,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
{PROJECT_PRIORITY_ORDER.map((pr) => (
<DropdownMenuItem key={pr} onClick={() => updatePriority(pr)}>
<PriorityIcon priority={pr} />
<span>{PROJECT_PRIORITY_CONFIG[pr].label}</span>
<span>{projectPriorityLabels[pr]}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -274,7 +286,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
<span>{leadLabel}</span>
</>
) : (
<span className="text-muted-foreground">Lead</span>
<span className="text-muted-foreground">{t(($) => $.create_project.lead)}</span>
)}
</PillButton>
}
@@ -285,7 +297,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
type="text"
value={leadFilter}
onChange={(e) => setLeadFilter(e.target.value)}
placeholder="Assign lead..."
placeholder={t(($) => $.create_project.lead_placeholder)}
className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none"
/>
</div>
@@ -299,12 +311,12 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
>
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">No lead</span>
<span className="text-muted-foreground">{t(($) => $.create_project.no_lead)}</span>
</button>
{filteredMembers.length > 0 && (
<>
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
Members
{t(($) => $.create_project.members_group)}
</div>
{filteredMembers.map((m) => (
<button
@@ -325,7 +337,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
{filteredAgents.length > 0 && (
<>
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
Agents
{t(($) => $.create_project.agents_group)}
</div>
{filteredAgents.map((a) => (
<button
@@ -347,7 +359,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
filteredAgents.length === 0 &&
leadFilter && (
<div className="px-2 py-3 text-center text-sm text-muted-foreground">
No results
{t(($) => $.create_project.no_results)}
</div>
)}
</div>
@@ -357,7 +369,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
<div className="flex items-center justify-end px-4 py-3 border-t shrink-0">
<Button size="sm" onClick={handleSubmit} disabled={!title.trim() || submitting}>
{submitting ? "Creating..." : "Create Project"}
{submitting ? t(($) => $.create_project.submitting) : t(($) => $.create_project.submit)}
</Button>
</div>
</DialogContent>

View File

@@ -12,8 +12,10 @@ import {
} from "@multica/ui/components/ui/dialog";
import { paths } from "@multica/core/paths";
import { CreateWorkspaceForm } from "../workspace/create-workspace-form";
import { useT } from "../i18n";
export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
const { t } = useT("modals");
const router = useNavigation();
return (
@@ -43,18 +45,17 @@ export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
onClick={onClose}
>
<ArrowLeft className="h-4 w-4" />
Back
{t(($) => $.common.back)}
</Button>
<div className="flex flex-1 flex-col items-center justify-center px-6 pb-12">
<div className="flex w-full max-w-md flex-col items-center gap-6">
<div className="text-center">
<DialogTitle className="text-2xl font-semibold">
Create a new workspace
{t(($) => $.create_workspace.title)}
</DialogTitle>
<DialogDescription className="mt-2">
Workspaces are shared environments where teams can work on
projects and issues.
{t(($) => $.create_workspace.description)}
</DialogDescription>
</div>
<CreateWorkspaceForm

View File

@@ -14,6 +14,7 @@ import {
} from "@multica/ui/components/ui/alert-dialog";
import { useDeleteIssue } from "@multica/core/issues/mutations";
import { useNavigation } from "../navigation";
import { useT } from "../i18n";
export function DeleteIssueConfirmModal({
onClose,
@@ -22,6 +23,7 @@ export function DeleteIssueConfirmModal({
onClose: () => void;
data: Record<string, unknown> | null;
}) {
const { t } = useT("modals");
const issueId = (data?.issueId as string) || "";
const navigateTo = (data?.onDeletedNavigateTo as string | undefined) || undefined;
const [deleting, setDeleting] = useState(false);
@@ -33,11 +35,11 @@ export function DeleteIssueConfirmModal({
setDeleting(true);
try {
await deleteIssue.mutateAsync(issueId);
toast.success("Issue deleted");
toast.success(t(($) => $.delete_issue.toast_deleted));
onClose();
if (navigateTo) navigation.push(navigateTo);
} catch {
toast.error("Failed to delete issue");
toast.error(t(($) => $.delete_issue.toast_delete_failed));
setDeleting(false);
}
};
@@ -46,22 +48,22 @@ export function DeleteIssueConfirmModal({
<AlertDialog open onOpenChange={(v) => { if (!v && !deleting) onClose(); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete issue</AlertDialogTitle>
<AlertDialogTitle>{t(($) => $.delete_issue.title)}</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this issue and all its comments. This action cannot be undone.
{t(($) => $.delete_issue.description)}
<span className="mt-2 block text-xs text-muted-foreground/80">
Any workspace member can delete issues.
{t(($) => $.delete_issue.hint)}
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={deleting}>{t(($) => $.delete_issue.cancel)}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={deleting}
className="bg-destructive text-white hover:bg-destructive/90"
>
{deleting ? "Deleting..." : "Delete"}
{deleting ? t(($) => $.delete_issue.deleting) : t(($) => $.delete_issue.confirm)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -21,10 +21,12 @@ import { useCurrentWorkspace } from "@multica/core/paths";
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
import { api } from "@multica/core/api";
import { captureFeedbackOpened } from "@multica/core/analytics";
import { useT } from "../i18n";
const MAX_MESSAGE_LEN = 10000;
export function FeedbackModal({ onClose }: { onClose: () => void }) {
const { t } = useT("modals");
const workspace = useCurrentWorkspace();
const draft = useFeedbackDraftStore((s) => s.draft);
const setDraft = useFeedbackDraftStore((s) => s.setDraft);
@@ -55,7 +57,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
const handleSubmit = async () => {
if (!canSubmit) return;
if (editorRef.current?.hasActiveUploads()) {
toast.info("Please wait for uploads to finish…");
toast.info(t(($) => $.feedback.toast_uploading));
return;
}
// Read from the editor ref at submit time — `message` state lags 150ms
@@ -64,7 +66,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
const latest = editorRef.current?.getMarkdown()?.trim() ?? "";
if (!latest) return;
if (latest.length > MAX_MESSAGE_LEN) {
toast.error("Message is too long");
toast.error(t(($) => $.feedback.toast_too_long));
return;
}
try {
@@ -74,13 +76,13 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
workspace_id: workspace?.id,
});
clearDraft();
toast.success("Thanks for the feedback!");
toast.success(t(($) => $.feedback.toast_sent));
onClose();
} catch (err) {
const msg =
err instanceof Error && err.message
? err.message
: "Failed to send feedback";
: t(($) => $.feedback.toast_failed);
toast.error(msg);
}
};
@@ -89,10 +91,9 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
<Dialog open onOpenChange={(v) => !v && onClose()}>
<DialogContent className="sm:max-w-2xl !h-[28rem] p-0 gap-0 flex flex-col overflow-hidden">
<DialogHeader className="px-5 pt-4 pb-2 shrink-0">
<DialogTitle>Feedback</DialogTitle>
<DialogTitle>{t(($) => $.feedback.title)}</DialogTitle>
<DialogDescription>
We&apos;d love to hear what&apos;s working, what isn&apos;t, or
what you&apos;d like to see next.
{t(($) => $.feedback.description)}
</DialogDescription>
</DialogHeader>
@@ -104,7 +105,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
<ContentEditor
ref={editorRef}
defaultValue={draft.message}
placeholder="Tell us about your experience, bugs you've found, or features you'd like to see…"
placeholder={t(($) => $.feedback.placeholder)}
onUpdate={(md) => { setMessage(md); setDraft({ message: md }); }}
onUploadFile={uploadWithToast}
onSubmit={handleSubmit}
@@ -118,7 +119,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
<div className="flex items-center justify-end px-4 py-3 border-t shrink-0">
<Button size="sm" onClick={handleSubmit} disabled={!canSubmit}>
{mutation.isPending ? "Sending…" : "Send feedback"}
{mutation.isPending ? t(($) => $.feedback.sending) : t(($) => $.feedback.send)}
<kbd className="ml-1 inline-flex h-4 items-center gap-0.5 rounded border border-border/50 bg-background/30 px-1 font-mono text-[10px] leading-none">
</kbd>

View File

@@ -13,6 +13,7 @@ import {
CommandItem,
} from "@multica/ui/components/ui/command";
import { StatusIcon } from "../issues/components/status-icon";
import { useT } from "../i18n";
interface IssuePickerModalProps {
open: boolean;
@@ -31,6 +32,7 @@ export function IssuePickerModal({
excludeIds,
onSelect,
}: IssuePickerModalProps) {
const { t } = useT("modals");
const [query, setQuery] = useState("");
const [results, setResults] = useState<Issue[]>([]);
const [isLoading, setIsLoading] = useState(false);
@@ -90,7 +92,7 @@ export function IssuePickerModal({
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search issues..."
placeholder={t(($) => $.issue_picker.search_placeholder)}
value={query}
onValueChange={(v) => {
setQuery(v);
@@ -100,15 +102,15 @@ export function IssuePickerModal({
<CommandList>
{isLoading && (
<div className="py-6 text-center text-sm text-muted-foreground">
Searching...
{t(($) => $.issue_picker.searching)}
</div>
)}
{!isLoading && query.trim() && results.length === 0 && (
<CommandEmpty>No issues found.</CommandEmpty>
<CommandEmpty>{t(($) => $.issue_picker.no_results)}</CommandEmpty>
)}
{!isLoading && !query.trim() && (
<div className="py-6 text-center text-sm text-muted-foreground">
Type to search issues
{t(($) => $.issue_picker.prompt_to_search)}
</div>
)}
{results.length > 0 && (

View File

@@ -39,6 +39,7 @@ import {
FileDropOverlay,
} from "../editor";
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
import { useT } from "../i18n";
// AgentCreatePanel — agent-mode body of the create-issue dialog. Renders
// only the inner content; the surrounding `<Dialog>` AND `<DialogContent>`
@@ -58,6 +59,7 @@ export function AgentCreatePanel({
onSwitchMode?: () => void;
data?: Record<string, unknown> | null;
}) {
const { t } = useT("modals");
const workspaceName = useCurrentWorkspace()?.name;
const wsId = useWorkspaceId();
const userId = useAuthStore((s) => s.user?.id);
@@ -167,7 +169,7 @@ export function AgentCreatePanel({
await api.quickCreateIssue({ agent_id: agentId, prompt: md });
setLastAgentId(agentId);
setLastMode("agent");
toast.success("Sent to agent — you'll get an inbox notification when it's done", {
toast.success(t(($) => $.create_issue.agent.toast_sent), {
duration: 4000,
});
if (keepOpen) {
@@ -195,7 +197,7 @@ export function AgentCreatePanel({
min_version?: string;
};
if (body.code === "agent_unavailable") {
setError(body.reason || "Agent is unavailable. Pick another agent.");
setError(body.reason || t(($) => $.create_issue.agent.error_agent_unavailable_fallback));
setSubmitting(false);
return;
}
@@ -206,13 +208,16 @@ export function AgentCreatePanel({
// consistency.
const cur = body.current_version || "unknown";
setError(
`This agent's daemon CLI (${cur}) is below the required ${body.min_version || MIN_QUICK_CREATE_CLI_VERSION}. Upgrade the daemon to use Create with agent.`,
t(($) => $.create_issue.agent.error_daemon_version, {
current: cur,
min: body.min_version || MIN_QUICK_CREATE_CLI_VERSION,
}),
);
setSubmitting(false);
return;
}
}
setError("Failed to submit. Try again.");
setError(t(($) => $.create_issue.agent.error_unknown));
} finally {
setSubmitting(false);
}
@@ -238,14 +243,14 @@ export function AgentCreatePanel({
return (
<>
<DialogTitle className="sr-only">Quick create issue</DialogTitle>
<DialogTitle className="sr-only">{t(($) => $.create_issue.sr_agent)}</DialogTitle>
{/* Header */}
<div className="flex items-center justify-between px-5 pt-3 pb-2 shrink-0">
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">{workspaceName}</span>
<ChevronRight className="size-3 text-muted-foreground/50" />
<span className="font-medium">Create with agent</span>
<span className="font-medium">{t(($) => $.create_issue.agent_breadcrumb)}</span>
</div>
{/* Native `title` instead of Base UI Tooltip — Tooltip opens on
keyboard focus, and the dialog's focus trap briefly lands focus
@@ -254,8 +259,8 @@ export function AgentCreatePanel({
<button
type="button"
onClick={onClose}
title="Close"
aria-label="Close"
title={t(($) => $.common.close)}
aria-label={t(($) => $.common.close)}
className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
>
<XIcon className="size-4" />
@@ -269,10 +274,10 @@ export function AgentCreatePanel({
render={
<button
type="button"
aria-label="Select agent"
aria-label={t(($) => $.create_issue.agent.select_agent_aria)}
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer rounded-sm px-1.5 py-1 -ml-1.5 hover:bg-accent/60"
>
<span>Created by</span>
<span>{t(($) => $.create_issue.agent.created_by)}</span>
{selectedAgent ? (
<span className="flex items-center gap-1.5 text-foreground">
<ActorAvatar
@@ -283,7 +288,7 @@ export function AgentCreatePanel({
{selectedAgent.name}
</span>
) : (
<span>Pick an agent</span>
<span>{t(($) => $.create_issue.agent.pick_an_agent)}</span>
)}
</button>
}
@@ -291,7 +296,7 @@ export function AgentCreatePanel({
<DropdownMenuContent align="start" className="w-64 max-h-72 overflow-y-auto">
{visibleAgents.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
No agents available.
{t(($) => $.create_issue.agent.no_agents)}
</div>
) : (
visibleAgents.map((a: Agent) => (
@@ -322,8 +327,11 @@ export function AgentCreatePanel({
{selectedAgent && versionBlocked && (
<div className="mx-5 mb-2 shrink-0 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
{versionCheck.state === "missing"
? `This agent's daemon doesn't report a CLI version. Create with agent needs multica CLI ≥ ${versionCheck.min}. Upgrade the daemon and reconnect, or switch to manual create.`
: `This agent's daemon CLI is ${versionCheck.current} — Create with agent needs ≥ ${versionCheck.min}. Upgrade the daemon, or switch to manual create.`}
? t(($) => $.create_issue.agent.version_missing, { min: versionCheck.min })
: t(($) => $.create_issue.agent.version_below, {
current: versionCheck.current,
min: versionCheck.min,
})}
</div>
)}
@@ -341,7 +349,7 @@ export function AgentCreatePanel({
<ContentEditor
ref={editorRef}
defaultValue={initialPrompt}
placeholder='Tell the agent what to do, e.g. "let Bohan fix the inbox loading slowness in the Web project"'
placeholder={t(($) => $.create_issue.agent.prompt_placeholder)}
onUpdate={(md) => setHasContent(md.trim().length > 0)}
onUploadFile={handleUploadFile}
onSubmit={submit}
@@ -364,7 +372,7 @@ export function AgentCreatePanel({
/>
{keepOpen && sentCount > 0 && (
<span className="text-xs text-emerald-600 dark:text-emerald-400">
{sentCount} sent
{t(($) => $.create_issue.agent.sent_count, { count: sentCount })}
</span>
)}
</div>
@@ -372,11 +380,11 @@ export function AgentCreatePanel({
<button
type="button"
onClick={switchToManual}
title="Switch to manual create — fill the fields yourself"
title={t(($) => $.create_issue.switch_to_manual_tooltip)}
className="flex shrink-0 items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Switch to Manual
{t(($) => $.create_issue.switch_to_manual)}
</button>
<label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Switch
@@ -384,7 +392,7 @@ export function AgentCreatePanel({
checked={keepOpen}
onCheckedChange={setKeepOpen}
/>
Create another
{t(($) => $.create_issue.create_another)}
</label>
<Button
size="sm"
@@ -392,14 +400,14 @@ export function AgentCreatePanel({
disabled={!hasContent || !agentId || submitting || versionBlocked || uploading}
title={
versionBlocked
? `Daemon CLI must be ≥ ${versionCheck.min}`
? t(($) => $.create_issue.agent.version_blocked_tooltip, { min: versionCheck.min })
: undefined
}
className={justSent ? "min-w-28 !bg-emerald-600 !text-white" : "min-w-28"}
>
{submitting ? "Sending…" : uploading ? "Uploading…" : justSent ? (
<span className="flex items-center gap-1"><Check className="size-3.5" />Sent</span>
) : "Create (⌘↵)"}
{submitting ? t(($) => $.create_issue.agent.sending) : uploading ? t(($) => $.create_issue.agent.uploading) : justSent ? (
<span className="flex items-center gap-1"><Check className="size-3.5" />{t(($) => $.create_issue.agent.sent_label)}</span>
) : t(($) => $.create_issue.agent.submit)}
</Button>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import { useWorkspaceId } from "@multica/core/hooks";
import { childIssuesOptions } from "@multica/core/issues/queries";
import { useUpdateIssue } from "@multica/core/issues/mutations";
import { IssuePickerModal } from "./issue-picker-modal";
import { useT } from "../i18n";
export function SetParentIssueModal({
onClose,
@@ -14,6 +15,7 @@ export function SetParentIssueModal({
onClose: () => void;
data: Record<string, unknown> | null;
}) {
const { t } = useT("modals");
const issueId = (data?.issueId as string) || "";
const wsId = useWorkspaceId();
const updateIssue = useUpdateIssue();
@@ -31,15 +33,15 @@ export function SetParentIssueModal({
onOpenChange={(v) => {
if (!v) onClose();
}}
title="Set parent issue"
description="Search for an issue to set as the parent of this issue"
title={t(($) => $.set_parent.title)}
description={t(($) => $.set_parent.description)}
excludeIds={excludeIds}
onSelect={(selected) => {
updateIssue.mutate(
{ id: issueId, parent_issue_id: selected.id },
{ onError: () => toast.error("Failed to update issue") },
{ onError: () => toast.error(t(($) => $.set_parent.toast_failed)) },
);
toast.success(`Set ${selected.identifier} as parent issue`);
toast.success(t(($) => $.set_parent.toast_success, { identifier: selected.identifier }));
}}
/>
);