diff --git a/packages/views/eslint.config.mjs b/packages/views/eslint.config.mjs index 019c7a3dc0..cedc14383f 100644 --- a/packages/views/eslint.config.mjs +++ b/packages/views/eslint.config.mjs @@ -82,6 +82,9 @@ const TRANSLATED_FILES = [ "runtimes/components/runtime-detail-page.tsx", "runtimes/components/runtime-detail.tsx", "runtimes/components/shared.tsx", + "layout/app-sidebar.tsx", + "layout/help-launcher.tsx", + "layout/workspace-loader.tsx", ]; export default [ diff --git a/packages/views/i18n/resources-types.ts b/packages/views/i18n/resources-types.ts index 6492250221..71ab6d79d2 100644 --- a/packages/views/i18n/resources-types.ts +++ b/packages/views/i18n/resources-types.ts @@ -19,6 +19,7 @@ import type skills from "../locales/en/skills.json"; import type chat from "../locales/en/chat.json"; import type modals from "../locales/en/modals.json"; import type runtimes from "../locales/en/runtimes.json"; +import type layout from "../locales/en/layout.json"; // Module augmentation enables i18next v26 selector API across the monorepo: // `t($ => $.signin.title)` resolves to the value in en/auth.json. @@ -52,6 +53,7 @@ declare module "i18next" { chat: typeof chat; modals: typeof modals; runtimes: typeof runtimes; + layout: typeof layout; }; enableSelector: true; } diff --git a/packages/views/layout/app-sidebar.tsx b/packages/views/layout/app-sidebar.tsx index 346194964d..eb9c1e0832 100644 --- a/packages/views/layout/app-sidebar.tsx +++ b/packages/views/layout/app-sidebar.tsx @@ -76,6 +76,7 @@ import { projectDetailOptions } from "@multica/core/projects/queries"; import type { PinnedItem } from "@multica/core/types"; import { useLogout } from "../auth"; import { ProjectIcon } from "../projects/components/project-icon"; +import { useT } from "../i18n"; // Top-level nav items stay active when the user is on a child route // (e.g. "Projects" stays lit on /:slug/projects/:id). Pinned items keep @@ -109,22 +110,34 @@ type NavKey = | "skills" | "settings"; -const personalNav: { key: NavKey; label: string; icon: typeof Inbox }[] = [ - { key: "inbox", label: "Inbox", icon: Inbox }, - { key: "myIssues", label: "My Issues", icon: CircleUser }, +// Static schema (key + icon) — labels resolved at render via useT("layout"). +type NavLabelKey = + | "inbox" + | "my_issues" + | "issues" + | "projects" + | "autopilots" + | "agents" + | "runtimes" + | "skills" + | "settings"; + +const personalNav: { key: NavKey; labelKey: NavLabelKey; icon: typeof Inbox }[] = [ + { key: "inbox", labelKey: "inbox", icon: Inbox }, + { key: "myIssues", labelKey: "my_issues", icon: CircleUser }, ]; -const workspaceNav: { key: NavKey; label: string; icon: typeof Inbox }[] = [ - { key: "issues", label: "Issues", icon: ListTodo }, - { key: "projects", label: "Projects", icon: FolderKanban }, - { key: "autopilots", label: "Autopilot", icon: Zap }, - { key: "agents", label: "Agents", icon: Bot }, +const workspaceNav: { key: NavKey; labelKey: NavLabelKey; icon: typeof Inbox }[] = [ + { key: "issues", labelKey: "issues", icon: ListTodo }, + { key: "projects", labelKey: "projects", icon: FolderKanban }, + { key: "autopilots", labelKey: "autopilots", icon: Zap }, + { key: "agents", labelKey: "agents", icon: Bot }, ]; -const configureNav: { key: NavKey; label: string; icon: typeof Inbox }[] = [ - { key: "runtimes", label: "Runtimes", icon: Monitor }, - { key: "skills", label: "Skills", icon: BookOpenText }, - { key: "settings", label: "Settings", icon: Settings }, +const configureNav: { key: NavKey; labelKey: NavLabelKey; icon: typeof Inbox }[] = [ + { key: "runtimes", labelKey: "runtimes", icon: Monitor }, + { key: "skills", labelKey: "skills", icon: BookOpenText }, + { key: "settings", labelKey: "settings", icon: Settings }, ]; function DraftDot() { @@ -154,6 +167,7 @@ function SortablePinItem({ label: string; iconNode: React.ReactNode; }) { + const { t } = useT("layout"); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: pin.id }); const wasDragged = useRef(false); @@ -208,7 +222,7 @@ function SortablePinItem({ > - Unpin + {t(($) => $.sidebar.unpin_tooltip)} @@ -308,6 +322,7 @@ interface AppSidebarProps { } export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }: AppSidebarProps = {}) { + const { t } = useT("layout"); const { pathname, push } = useNavigation(); const user = useAuthStore((s) => s.user); const userId = useAuthStore((s) => s.user?.id); @@ -589,8 +604,8 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle } className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground" > - {item.label} - {item.label === "Inbox" && unreadCount > 0 && ( + {t(($) => $.nav[item.labelKey])} + {item.key === "inbox" && unreadCount > 0 && ( {unreadCount > 99 ? "99+" : unreadCount} @@ -653,7 +668,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle } className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground" > - {item.label} + {t(($) => $.nav[item.labelKey])} ); @@ -677,8 +692,8 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle } className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground" > - {item.label} - {item.label === "Runtimes" && hasRuntimeUpdates && ( + {t(($) => $.nav[item.labelKey])} + {item.key === "runtimes" && hasRuntimeUpdates && ( )} diff --git a/packages/views/layout/help-launcher.tsx b/packages/views/layout/help-launcher.tsx index dee17c3a48..4f8464d5f7 100644 --- a/packages/views/layout/help-launcher.tsx +++ b/packages/views/layout/help-launcher.tsx @@ -8,16 +8,18 @@ import { DropdownMenuTrigger, } from "@multica/ui/components/ui/dropdown-menu"; import { useModalStore } from "@multica/core/modals"; +import { useT } from "../i18n"; const DOCS_URL = "https://multica.ai/docs"; const CHANGELOG_URL = "https://multica.ai/changelog"; export function HelpLauncher() { + const { t } = useT("layout"); return ( $.help.trigger)} + title={t(($) => $.help.trigger)} className="inline-flex size-7 items-center justify-center rounded-full text-muted-foreground transition-colors cursor-pointer hover:bg-accent hover:text-foreground data-popup-open:bg-accent data-popup-open:text-foreground" > @@ -34,7 +36,7 @@ export function HelpLauncher() { } > - Docs + {t(($) => $.help.docs)} - Change log + {t(($) => $.help.changelog)} useModalStore.getState().open("feedback")} > - Feedback + {t(($) => $.help.feedback)} diff --git a/packages/views/layout/workspace-loader.tsx b/packages/views/layout/workspace-loader.tsx index 133f4b4c62..306f5a8de9 100644 --- a/packages/views/layout/workspace-loader.tsx +++ b/packages/views/layout/workspace-loader.tsx @@ -1,6 +1,7 @@ "use client"; import { MulticaIcon } from "@multica/ui/components/common/multica-icon"; +import { useT } from "../i18n"; /** * Full-screen workspace loader. Renders IN PLACE OF the dashboard during: @@ -13,6 +14,7 @@ import { MulticaIcon } from "@multica/ui/components/common/multica-icon"; * workspace have been freshly fetched. */ export function WorkspaceLoader({ name }: { name?: string | null }) { + const { t } = useT("layout"); return (
{name ? (

- Loading {name}… + {t(($) => $.workspace_loader.loading_named_prefix)}{" "} + {name}

) : ( -

Loading workspace…

+

{t(($) => $.workspace_loader.loading_workspace)}

)}
diff --git a/packages/views/locales/en/layout.json b/packages/views/locales/en/layout.json new file mode 100644 index 0000000000..5fd569ba09 --- /dev/null +++ b/packages/views/locales/en/layout.json @@ -0,0 +1,26 @@ +{ + "nav": { + "inbox": "Inbox", + "my_issues": "My Issues", + "issues": "Issues", + "projects": "Projects", + "autopilots": "Autopilot", + "agents": "Agents", + "runtimes": "Runtimes", + "skills": "Skills", + "settings": "Settings" + }, + "help": { + "trigger": "Help", + "docs": "Docs", + "changelog": "Change log", + "feedback": "Feedback" + }, + "workspace_loader": { + "loading_workspace": "Loading workspace…", + "loading_named_prefix": "Loading" + }, + "sidebar": { + "unpin_tooltip": "Unpin" + } +} diff --git a/packages/views/locales/index.ts b/packages/views/locales/index.ts index 5e54c43aaf..079ad4dc5c 100644 --- a/packages/views/locales/index.ts +++ b/packages/views/locales/index.ts @@ -16,6 +16,7 @@ import enSkills from "./en/skills.json"; import enChat from "./en/chat.json"; import enModals from "./en/modals.json"; import enRuntimes from "./en/runtimes.json"; +import enLayout from "./en/layout.json"; import zhHansCommon from "./zh-Hans/common.json"; import zhHansAuth from "./zh-Hans/auth.json"; import zhHansSettings from "./zh-Hans/settings.json"; @@ -33,6 +34,7 @@ import zhHansSkills from "./zh-Hans/skills.json"; import zhHansChat from "./zh-Hans/chat.json"; import zhHansModals from "./zh-Hans/modals.json"; import zhHansRuntimes from "./zh-Hans/runtimes.json"; +import zhHansLayout from "./zh-Hans/layout.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 @@ -56,6 +58,7 @@ export const RESOURCES: Record = { chat: enChat, modals: enModals, runtimes: enRuntimes, + layout: enLayout, }, "zh-Hans": { common: zhHansCommon, @@ -75,5 +78,6 @@ export const RESOURCES: Record = { chat: zhHansChat, modals: zhHansModals, runtimes: zhHansRuntimes, + layout: zhHansLayout, }, }; diff --git a/packages/views/locales/zh-Hans/layout.json b/packages/views/locales/zh-Hans/layout.json new file mode 100644 index 0000000000..1fd0cf72b6 --- /dev/null +++ b/packages/views/locales/zh-Hans/layout.json @@ -0,0 +1,26 @@ +{ + "nav": { + "inbox": "收件箱", + "my_issues": "我的 issue", + "issues": "issue", + "projects": "project", + "autopilots": "autopilot", + "agents": "智能体", + "runtimes": "运行时", + "skills": "skill", + "settings": "设置" + }, + "help": { + "trigger": "帮助", + "docs": "文档", + "changelog": "更新日志", + "feedback": "反馈" + }, + "workspace_loader": { + "loading_workspace": "正在加载工作区…", + "loading_named_prefix": "正在加载" + }, + "sidebar": { + "unpin_tooltip": "取消固定" + } +}