diff --git a/packages/views/eslint.config.mjs b/packages/views/eslint.config.mjs index b7cd15f4b3..d7cbc7acf7 100644 --- a/packages/views/eslint.config.mjs +++ b/packages/views/eslint.config.mjs @@ -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 [ diff --git a/packages/views/i18n/resources-types.ts b/packages/views/i18n/resources-types.ts index 1b24b9a2f4..80cfc91d42 100644 --- a/packages/views/i18n/resources-types.ts +++ b/packages/views/i18n/resources-types.ts @@ -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; } diff --git a/packages/views/locales/en/modals.json b/packages/views/locales/en/modals.json new file mode 100644 index 0000000000..fcf8845792 --- /dev/null +++ b/packages/views/locales/en/modals.json @@ -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." + } + } +} diff --git a/packages/views/locales/index.ts b/packages/views/locales/index.ts index 97172d9517..976cfe85bc 100644 --- a/packages/views/locales/index.ts +++ b/packages/views/locales/index.ts @@ -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 = { autopilots: enAutopilots, skills: enSkills, chat: enChat, + modals: enModals, }, "zh-Hans": { common: zhHansCommon, @@ -67,5 +70,6 @@ export const RESOURCES: Record = { autopilots: zhHansAutopilots, skills: zhHansSkills, chat: zhHansChat, + modals: zhHansModals, }, }; diff --git a/packages/views/locales/zh-Hans/modals.json b/packages/views/locales/zh-Hans/modals.json new file mode 100644 index 0000000000..022d92a266 --- /dev/null +++ b/packages/views/locales/zh-Hans/modals.json @@ -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": "提交失败,请重试。" + } + } +} diff --git a/packages/views/modals/add-child-issue.tsx b/packages/views/modals/add-child-issue.tsx index 2a2bf4d331..f7ec9d0768 100644 --- a/packages/views/modals/add-child-issue.tsx +++ b/packages/views/modals/add-child-issue.tsx @@ -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 | 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 })); }} /> ); diff --git a/packages/views/modals/backlog-agent-hint.tsx b/packages/views/modals/backlog-agent-hint.tsx index 97b8b4fbed..76be3c3f65 100644 --- a/packages/views/modals/backlog-agent-hint.tsx +++ b/packages/views/modals/backlog-agent-hint.tsx @@ -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 | 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(); diff --git a/packages/views/modals/create-issue.test.tsx b/packages/views/modals/create-issue.test.tsx index 6955447d79..fa68b44881 100644 --- a/packages/views/modals/create-issue.test.tsx +++ b/packages/views/modals/create-issue.test.tsx @@ -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 ( + + {children} + + ); +} 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({element}); + return render( + + {element} + , + ); } describe("CreateIssueModal", () => { diff --git a/packages/views/modals/create-issue.tsx b/packages/views/modals/create-issue.tsx index 9462238eb7..3f252e07bc 100644 --- a/packages/views/modals/create-issue.tsx +++ b/packages/views/modals/create-issue.tsx @@ -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) => (
- Issue created + {t(($) => $.create_issue.toast_created)}
@@ -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)}
), { 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({ /> ) : ( <> - New Issue + {t(($) => $.create_issue.sr_manual)} {/* Header */}
{workspaceName} - Create manually + {t(($) => $.create_issue.manual_breadcrumb)}
@@ -312,7 +317,11 @@ export function ManualCreatePanel({ } /> - {isExpanded ? "Collapse" : "Expand"} + + {isExpanded + ? t(($) => $.common.collapse_tooltip) + : t(($) => $.common.expand_tooltip)} + } /> - Close + {t(($) => $.common.close)}
@@ -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({ $.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" > - Sub-issue of {parentIssue.identifier} + + {t(($) => $.create_issue.subissue_of, { identifier: parentIssue.identifier })} + @@ -435,7 +446,7 @@ export function ManualCreatePanel({ >
- Sub-issue: {c.identifier} + {t(($) => $.create_issue.subissue_chip, { identifier: c.identifier })}
@@ -455,7 +466,7 @@ export function ManualCreatePanel({ + $.create_issue.more_options_aria)}> } @@ -464,17 +475,17 @@ export function ManualCreatePanel({ {parentIssueId && parentIssue ? ( setParentPickerOpen(true)}> - Parent: {parentIssue.identifier} + {t(($) => $.create_issue.parent_with_id, { identifier: parentIssue.identifier })} ) : ( setParentPickerOpen(true)}> - Set parent issue... + {t(($) => $.create_issue.set_parent)} )} setChildPickerOpen(true)}> - Add sub-issue... + {t(($) => $.create_issue.add_subissue)} {parentIssueId && parentIssue && ( <> @@ -484,7 +495,7 @@ export function ManualCreatePanel({ onClick={() => setParentIssueId(undefined)} > - Remove parent + {t(($) => $.create_issue.remove_parent)} )} @@ -497,8 +508,8 @@ export function ManualCreatePanel({ $.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({ $.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({
diff --git a/packages/views/modals/create-project.tsx b/packages/views/modals/create-project.tsx index e0d33ff975..22323e4379 100644 --- a/packages/views/modals/create-project.tsx +++ b/packages/views/modals/create-project.tsx @@ -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", )} > - New Project + {t(($) => $.create_project.title)}
{workspaceName} - New project + {t(($) => $.create_project.title_breadcrumb)}
@@ -159,7 +167,11 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { } /> - {isExpanded ? "Collapse" : "Expand"} + + {isExpanded + ? t(($) => $.common.collapse_tooltip) + : t(($) => $.common.expand_tooltip)} + void }) { } /> - Close + {t(($) => $.common.close)}
@@ -184,7 +196,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { @@ -202,7 +214,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { $.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 }) { $.create_project.description_placeholder)} onUpdate={(md) => setDraft({ description: md })} debounceMs={500} /> @@ -225,7 +237,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { render={ - {PROJECT_STATUS_CONFIG[status].label} + {projectStatusLabels[status]} } /> @@ -233,7 +245,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { {PROJECT_STATUS_ORDER.map((s) => ( updateStatus(s)}> - {PROJECT_STATUS_CONFIG[s].label} + {projectStatusLabels[s]} ))} @@ -244,7 +256,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { render={ - {PROJECT_PRIORITY_CONFIG[priority].label} + {projectPriorityLabels[priority]} } /> @@ -252,7 +264,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { {PROJECT_PRIORITY_ORDER.map((pr) => ( updatePriority(pr)}> - {PROJECT_PRIORITY_CONFIG[pr].label} + {projectPriorityLabels[pr]} ))} @@ -274,7 +286,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { {leadLabel} ) : ( - Lead + {t(($) => $.create_project.lead)} )} } @@ -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" /> @@ -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" > - No lead + {t(($) => $.create_project.no_lead)} {filteredMembers.length > 0 && ( <>
- Members + {t(($) => $.create_project.members_group)}
{filteredMembers.map((m) => ( diff --git a/packages/views/modals/create-workspace.tsx b/packages/views/modals/create-workspace.tsx index a61d225211..5739377ce6 100644 --- a/packages/views/modals/create-workspace.tsx +++ b/packages/views/modals/create-workspace.tsx @@ -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} > - Back + {t(($) => $.common.back)}
- Create a new workspace + {t(($) => $.create_workspace.title)} - Workspaces are shared environments where teams can work on - projects and issues. + {t(($) => $.create_workspace.description)}
void; data: Record | 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({ { if (!v && !deleting) onClose(); }}> - Delete issue + {t(($) => $.delete_issue.title)} - This will permanently delete this issue and all its comments. This action cannot be undone. + {t(($) => $.delete_issue.description)} - Any workspace member can delete issues. + {t(($) => $.delete_issue.hint)} - Cancel + {t(($) => $.delete_issue.cancel)} - {deleting ? "Deleting..." : "Delete"} + {deleting ? t(($) => $.delete_issue.deleting) : t(($) => $.delete_issue.confirm)} diff --git a/packages/views/modals/feedback.tsx b/packages/views/modals/feedback.tsx index a735c95bac..d73d07240d 100644 --- a/packages/views/modals/feedback.tsx +++ b/packages/views/modals/feedback.tsx @@ -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 }) { !v && onClose()}> - Feedback + {t(($) => $.feedback.title)} - We'd love to hear what's working, what isn't, or - what you'd like to see next. + {t(($) => $.feedback.description)} @@ -104,7 +105,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) { $.feedback.placeholder)} onUpdate={(md) => { setMessage(md); setDraft({ message: md }); }} onUploadFile={uploadWithToast} onSubmit={handleSubmit} @@ -118,7 +119,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
} @@ -291,7 +296,7 @@ export function AgentCreatePanel({ {visibleAgents.length === 0 ? (
- No agents available. + {t(($) => $.create_issue.agent.no_agents)}
) : ( visibleAgents.map((a: Agent) => ( @@ -322,8 +327,11 @@ export function AgentCreatePanel({ {selectedAgent && versionBlocked && (
{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, + })}
)} @@ -341,7 +349,7 @@ export function AgentCreatePanel({ $.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 && ( - {sentCount} sent + {t(($) => $.create_issue.agent.sent_count, { count: sentCount })} )}
@@ -372,11 +380,11 @@ export function AgentCreatePanel({
diff --git a/packages/views/modals/set-parent-issue.tsx b/packages/views/modals/set-parent-issue.tsx index 11c65f73a0..eb4c4b3c2a 100644 --- a/packages/views/modals/set-parent-issue.tsx +++ b/packages/views/modals/set-parent-issue.tsx @@ -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 | 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 })); }} /> );