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({
>
- 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