+
diff --git a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
new file mode 100644
index 00000000000..c95565e1869
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { Languages } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { isLanguage, LANGUAGE_STORAGE_KEY } from "@/i18n";
+import { useTranslation } from "@/i18n/useTranslation";
+
+export default function LanguageSwitcher() {
+ const { t, i18n } = useTranslation();
+
+ const selectLanguage = (language: string) => {
+ if (!isLanguage(language)) return;
+ void i18n.changeLanguage(language);
+ try {
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
+ } catch {}
+ };
+
+ return (
+
+ }
+ >
+
+
+
+
+
+ English
+
+
+ 简体中文
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
new file mode 100644
index 00000000000..f3ef3f267d8
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
@@ -0,0 +1,80 @@
+import { act } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { I18nextProvider } from "react-i18next";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createDashboardI18n } from "@/i18n";
+import { renderWithProviders, screen } from "../../../../tests/test-utils";
+import { BlogDropdown } from "./BlogDropdown";
+
+const mockRefetch = vi.fn();
+let mockIsError = false;
+
+vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({
+ useDisableBlogPosts: () => false,
+}));
+
+vi.mock("@/app/(dashboard)/hooks/blogPosts/useBlogPosts", () => ({
+ useBlogPosts: () => ({
+ data: {
+ posts: [{ title: "Post One", date: "2026-02-01", description: "Description one", url: "https://example.com/1" }],
+ },
+ isLoading: false,
+ isError: mockIsError,
+ refetch: mockRefetch,
+ }),
+}));
+
+async function openDropdown() {
+ await userEvent.setup().hover(screen.getByRole("button", { name: "Blog" }));
+}
+
+describe("blog dropdown language changes", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockIsError = false;
+ });
+
+ it("should translate Retry after switching languages and still refetch posts", async () => {
+ mockIsError = true;
+ const user = userEvent.setup();
+ const i18n = createDashboardI18n();
+ renderWithProviders(
+
+
+ ,
+ );
+
+ await openDropdown();
+ expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("zh-CN");
+ });
+ expect(screen.getByText("加载文章失败")).toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "重试" }));
+ expect(mockRefetch).toHaveBeenCalledTimes(1);
+ });
+
+ it("should update visible dates when switching between English and Chinese", async () => {
+ const i18n = createDashboardI18n();
+ renderWithProviders(
+
+
+ ,
+ );
+
+ await openDropdown();
+ expect(await screen.findByText("Feb 1, 2026")).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("zh-CN");
+ });
+ expect(screen.getByText("2026年2月1日")).toBeInTheDocument();
+ expect(screen.queryByText("Feb 1, 2026")).not.toBeInTheDocument();
+ expect(screen.getByText("Post One")).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("en");
+ });
+ expect(screen.getByText("Feb 1, 2026")).toBeInTheDocument();
+ expect(screen.queryByText("2026年2月1日")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
index eaaf3301a8b..19880e07b16 100644
--- a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useBlogPosts, type BlogPost } from "@/app/(dashboard)/hooks/blogPosts/useBlogPosts";
import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
@@ -12,9 +13,9 @@ import {
import { ChevronDown, LoaderCircle } from "lucide-react";
import React from "react";
-function formatDate(dateStr: string): string {
+function formatDate(dateStr: string, language: string): string {
const date = new Date(dateStr + "T00:00:00");
- return date.toLocaleDateString("en-US", {
+ return date.toLocaleDateString(language === "zh-CN" ? "zh-CN" : "en-US", {
month: "short",
day: "numeric",
year: "numeric",
@@ -22,6 +23,7 @@ function formatDate(dateStr: string): string {
}
export const BlogDropdown: React.FC = () => {
+ const { t, i18n } = useTranslation();
const disableBlogPosts = useDisableBlogPosts();
const { data, isLoading, isError, refetch } = useBlogPosts();
@@ -34,7 +36,7 @@ export const BlogDropdown: React.FC = () => {
if (isLoading) {
return (
-
+
);
}
@@ -42,16 +44,16 @@ export const BlogDropdown: React.FC = () => {
if (isError) {
return (
- Failed to load posts
+ {t("Failed to load posts")}
refetch()}>
- Retry
+ {t("Retry")}
);
}
if (!data || data.posts.length === 0) {
- return
No posts available
;
+ return
{t("No posts available")}
;
}
return (
@@ -63,7 +65,7 @@ export const BlogDropdown: React.FC = () => {
{post.title}
- {formatDate(post.date)}
+ {formatDate(post.date, i18n.language)}
{post.description}
@@ -72,7 +74,7 @@ export const BlogDropdown: React.FC = () => {
- View all posts
+ {t("View all posts")}
>
@@ -87,8 +89,7 @@ export const BlogDropdown: React.FC = () => {
closeDelay={100}
render={
}
>
- Blog
-
+ {t("Blog")}
{renderMenuContent()}
diff --git a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
index 9b4f4c90b64..21118f6ff5d 100644
--- a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
import { ChevronDown } from "lucide-react";
import React from "react";
@@ -8,11 +9,13 @@ const ChevronWidthSpacer: React.FC = () => (
);
-export const DocsLink: React.FC = () => (
-
- Docs
-
-
-);
+export const DocsLink: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("Docs")}
+
+ );
+};
export default DocsLink;
diff --git a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.integration.test.tsx
new file mode 100644
index 00000000000..1f87b10ed47
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.integration.test.tsx
@@ -0,0 +1,50 @@
+import { act, render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { I18nextProvider } from "react-i18next";
+import { createDashboardI18n } from "@/i18n";
+import { AUTO_ROUTER_DOCS_URL, NotificationsBellView } from "./NotificationsBell";
+
+describe("localized announcements", () => {
+ beforeEach(() => localStorage.clear());
+ afterEach(() => localStorage.clear());
+
+ it("translates releases and unread counts while preserving independent read state and links", async () => {
+ const user = userEvent.setup();
+ const instance = createDashboardI18n("zh-CN");
+ const releaseUrl = "https://github.com/BerriAI/litellm/releases/tag/v1.103.0";
+ render(
+
+
+ ,
+ );
+ await user.click(screen.getByRole("button", { name: "通知,2 条未读" }));
+ const release = screen.getByRole("article", { name: "LiteLLM v1.103.0 已发布" });
+ expect(release).toHaveTextContent("1 项新功能、2 项修复和3 项其他更新。当前版本为 v1.102.0");
+ expect(within(release).getByRole("link", { name: "查看发行说明" })).toHaveAttribute("href", releaseUrl);
+ expect(screen.getByRole("link", { name: "查看文档" })).toHaveAttribute("href", AUTO_ROUTER_DOCS_URL);
+ await user.click(within(release).getByRole("button", { name: "标记为已读" }));
+ expect(screen.getByRole("button", { name: "通知,1 条未读" })).toBeInTheDocument();
+ expect(localStorage.getItem("litellm:upgradeBannerDismissed:1.103.0")).toBe("true");
+ expect(localStorage.getItem("litellmHideAutoRouterAnnouncement")).toBeNull();
+
+ await act(async () => {
+ await instance.changeLanguage("en");
+ });
+ expect(screen.getByRole("button", { name: "Notifications, 1 unread" })).toBeInTheDocument();
+ expect(screen.getByRole("article", { name: "LiteLLM v1.103.0 is available" })).toHaveTextContent(
+ "1 new feature, 2 fixes, and 3 other updates. Your current version is v1.102.0",
+ );
+ await user.click(screen.getByRole("button", { name: "Mark as read" }));
+ expect(screen.getByRole("button", { name: "Notifications", exact: true })).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
index 4cb9f851153..be5f313dcff 100644
--- a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useTranslation } from "@/i18n/useTranslation";
+import type { TFunction } from "i18next";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import {
type LatestReleaseInfo,
@@ -37,38 +39,39 @@ const AUTO_ROUTER_ANNOUNCEMENT: Announcement = {
href: AUTO_ROUTER_DOCS_URL,
};
-const plural = (count: number, singular: string, pluralForm: string): string =>
- `${count} ${count === 1 ? singular : pluralForm}`;
-
-const describeRelease = ({ new_features, bug_fixes, other_updates }: LatestReleaseInfo): string =>
- [
- plural(new_features, "new feature", "new features"),
- plural(bug_fixes, "fix", "fixes"),
- `and ${plural(other_updates, "other update", "other updates")}`,
- ].join(", ");
+const describeRelease = ({ new_features, bug_fixes, other_updates }: LatestReleaseInfo, t: TFunction): string =>
+ t("releaseSummary", {
+ features: t("releaseFeatures", { count: new_features }),
+ fixes: t("releaseFixes", { count: bug_fixes }),
+ updates: t("releaseUpdates", { count: other_updates }),
+ });
const upgradeAnnouncement = (
currentVersion: string | null | undefined,
latestRelease: LatestReleaseInfo | null | undefined,
+ t: TFunction,
): Announcement | null => {
if (!currentVersion || !latestRelease || !isNewerVersion(currentVersion, latestRelease.version)) {
return null;
}
return {
readKey: `litellm:upgradeBannerDismissed:${latestRelease.version}`,
- title: `LiteLLM v${latestRelease.version} is available`,
- description: `${describeRelease(latestRelease)}. Your current version is v${currentVersion}`,
+ title: t("releaseAvailable", { version: latestRelease.version }),
+ description: t("releaseDescription", { summary: describeRelease(latestRelease, t), version: currentVersion }),
linkLabel: "View release notes",
href: latestRelease.release_url,
};
};
const AnnouncementItem: React.FC<{ announcement: Announcement }> = ({ announcement }) => {
+ const { t } = useTranslation();
const read = useAnnouncementRead(announcement.readKey);
return (
-
- {announcement.title}
- {announcement.description}
+
+ {t(announcement.title, { defaultValue: announcement.title })}
+
+ {t(announcement.description, { defaultValue: announcement.description })}
+
@@ -94,7 +97,8 @@ interface NotificationsBellViewProps {
}
export const NotificationsBellView: React.FC = ({ currentVersion, latestRelease }) => {
- const upgrade = upgradeAnnouncement(currentVersion, latestRelease);
+ const { t } = useTranslation();
+ const upgrade = upgradeAnnouncement(currentVersion, latestRelease, t);
const announcements = upgrade ? [upgrade, AUTO_ROUTER_ANNOUNCEMENT] : [AUTO_ROUTER_ANNOUNCEMENT];
const unreadCount = useUnreadAnnouncementCount(announcements.map((announcement) => announcement.readKey));
@@ -102,14 +106,14 @@ export const NotificationsBellView: React.FC = ({ cu
0 ? `Notifications, ${unreadCount} unread` : "Notifications"}
+ aria-label={unreadCount > 0 ? t("notificationsUnread", { count: unreadCount }) : t("Notifications")}
>
{unreadCount > 0 ? : null}
-
+
{announcements.map((announcement, index) => (
{index > 0 ? : null}
diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
index 69117073ced..a704978ddc9 100644
--- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
@@ -79,6 +80,7 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
} = useAuthorized();
const router = useRouter();
const [open, setOpen] = useState(false);
+ const { t } = useTranslation();
const disableShowPrompts = useDisableShowPrompts();
const disableBlogPosts = useDisableBlogPosts();
const disableBouncingIcon = useDisableBouncingIcon();
@@ -101,16 +103,16 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
{premiumUser ? (
- Premium
+ {t("Premium")}
) : (
}>
- Standard
+ {t("Standard")}
- Upgrade to Premium for advanced features
+ {t("Upgrade to Premium for advanced features")}
)}
@@ -119,25 +121,25 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
- User ID
+ {t("User ID")}
{userId || "-"}
-
+
- Role
+ {t("Role")}
{userRole}
- Hide New Feature Indicators
+ {t("Hide New Feature Indicators")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableShowNewBadge");
}
}}
- aria-label="Toggle hide new feature indicators"
+ aria-label={t("Toggle hide new feature indicators")}
/>
- Hide All Prompts
+ {t("Hide All Prompts")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableShowPrompts");
}
}}
- aria-label="Toggle hide all prompts"
+ aria-label={t("Toggle hide all prompts")}
/>
- Hide Blog Posts
+ {t("Hide Blog Posts")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableBlogPosts");
}
}}
- aria-label="Toggle hide blog posts"
+ aria-label={t("Toggle hide blog posts")}
/>
- Hide Bouncing Icon
+ {t("Hide Bouncing Icon")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableBouncingIcon");
}
}}
- aria-label="Toggle hide bouncing icon"
+ aria-label={t("Toggle hide bouncing icon")}
/>
{premiumUser === true && canUseLiteAdmin && (
@@ -222,7 +224,7 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
const seed = userEmail || userId || "user";
const initials = initialsFromIdentity(userEmail, userId);
const hue = hueFromString(seed);
- const displayName = navAccountDisplayName(userEmail, userId);
+ const displayName = navAccountDisplayName(userEmail, userId, t("Account"));
return (
@@ -235,7 +237,10 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
"flex w-full items-center rounded-lg border border-transparent transition-colors hover:bg-sidebar-accent",
collapsed ? "justify-center px-0 py-1" : "gap-2.5 px-2 py-1.5 text-left",
)}
- aria-label={`Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`}
+ aria-label={t("accountMenu", {
+ role: userRole ?? t("Unknown role"),
+ identity: userEmail || userId || t("unknown"),
+ })}
aria-haspopup="dialog"
title={collapsed ? displayName : undefined}
/>
@@ -262,7 +267,10 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
}
@@ -305,7 +313,7 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
>
- Logout
+ {t("Logout")}
diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
index 8786d7df631..e117756f32d 100644
--- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
@@ -1,4 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { I18nextProvider } from "react-i18next";
+import { createDashboardI18n } from "@/i18n";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, testQueryClient, within } from "@/../tests/test-utils";
@@ -21,13 +23,15 @@ vi.mock("@/components/networking", () => ({
getUiConfig: getUiConfigMock,
}));
-function renderSwitcher() {
+function renderSwitcher(language: "en" | "zh-CN" = "en") {
return renderWithProviders(
-
-
-
-
- ,
+
+
+
+
+
+
+ ,
);
}
@@ -65,6 +69,29 @@ describe("ViewSwitcher", () => {
vi.unstubAllEnvs();
});
+ it.each([
+ ["/ui/chat", "聊天"],
+ ["/ui/moyai", "Moyai"],
+ ["/ui/", "AI 网关"],
+ ])("localizes the active workspace at %s while preserving product names", (pathname, label) => {
+ state.pathname = pathname;
+ renderSwitcher("zh-CN");
+ expect(screen.getByRole("button", { name: `切换工作区,当前:${label}` })).toHaveTextContent(label);
+ });
+
+ it("localizes workspace descriptions and disabled Chat without changing navigation", async () => {
+ const user = userEvent.setup();
+ renderSwitcher("zh-CN");
+ await user.click(screen.getByRole("button", { name: "切换工作区,当前:AI 网关" }));
+ const chat = await screen.findByRole("menuitem", {
+ name: "聊天 使用 LLM、MCP 和智能体的聊天界面 管理员可在设置中启用",
+ });
+ expect(chat).toHaveAttribute("aria-disabled", "true");
+ expect(assignSpy).not.toHaveBeenCalled();
+ await user.click(await screen.findByRole("menuitem", { name: "Moyai 云端编程智能体" }));
+ expect(assignSpy).toHaveBeenCalledWith("/ui/moyai");
+ });
+
it("should render the workspace selector and keep disabled Chat unavailable", async () => {
const user = userEvent.setup();
renderSwitcher();
@@ -88,6 +115,16 @@ describe("ViewSwitcher", () => {
expect(screen.queryByRole("menuitem", { name: "AI Gateway" })).not.toBeInTheDocument();
});
+ it("preserves SI terminology when the workspace selector is in Chinese", async () => {
+ getUiConfigMock.mockResolvedValue({ use_si_terminology: true });
+ const user = userEvent.setup();
+ renderSwitcher("zh-CN");
+
+ await user.click(await screen.findByRole("button", { name: "切换工作区,当前:SI 网关" }));
+ expect(await screen.findByRole("menuitem", { name: "SI 网关" })).toBeInTheDocument();
+ expect(screen.queryByRole("menuitem", { name: "AI 网关" })).not.toBeInTheDocument();
+ });
+
it("should show the active plugin and switch plugin modes within the gateway", async () => {
state.plugins = [
{ name: "metrics", display_name: "Metrics", url: "/metrics" },
diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
index 8469a8edc35..f7b61f4a8d7 100644
--- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { useTerminology } from "@/contexts/TerminologyContext";
import React from "react";
import Image from "next/image";
@@ -31,11 +32,11 @@ function activeWorkspaceFor(
isChatRoute: boolean,
isMoyaiRoute: boolean,
pluginLabel: string | undefined,
- gatewayLabel: string,
+ labels: { gateway: string; chat: string },
): { label: string; icon: WorkspaceIconName } {
- if (isChatRoute) return { label: "Chat", icon: CHAT };
+ if (isChatRoute) return { label: labels.chat, icon: CHAT };
if (isMoyaiRoute) return { label: "Moyai", icon: MOYAI };
- return { label: pluginLabel ?? gatewayLabel, icon: GATEWAY };
+ return { label: pluginLabel ?? labels.gateway, icon: GATEWAY };
}
function WorkspaceIcon({ workspace }: { workspace: WorkspaceIconName }) {
@@ -81,6 +82,7 @@ interface ViewSwitcherProps {
}
export default function ViewSwitcher({ className, contentClassName }: ViewSwitcherProps) {
+ const { t } = useTranslation();
const term = useTerminology();
const { mode, setMode, plugins } = usePluginMode();
const { data: uiSettings } = useUISettings();
@@ -94,12 +96,12 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
const isMoyaiRoute = isWorkspaceRoute(normalizedPathname, MOYAI);
const isStandaloneRoute = isChatRoute || isMoyaiRoute;
- const gatewayLabel = term("AI Gateway");
+ const gatewayLabel = term(t("AI Gateway"));
const { label: activeLabel, icon: activeIcon } = activeWorkspaceFor(
isChatRoute,
isMoyaiRoute,
plugins.find((p) => p.name === mode)?.display_name,
- gatewayLabel,
+ { gateway: gatewayLabel, chat: t("Chat") },
);
const modeEntries = [
@@ -122,11 +124,13 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
- Chat {" "}
- Chat UI for using LLMs, MCPs, Agents {" "}
+ {t("Chat")} {" "}
+
+ {t("Chat UI for using LLMs, MCPs, Agents")}
+ {" "}
{!chatEnabled && (
- Admins can enable in Settings
+ {t("Admins can enable in Settings")}
)}
@@ -160,7 +164,7 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
Moyai {" "}
- Cloud Coding Agent
+ {t("Cloud Coding Agent")}
{isMoyaiRoute && }
@@ -173,7 +177,7 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
return (
{
expect(navAccountDisplayName(null, "user-uuid-123")).toBe("user-uuid-123");
});
});
+
+it("localizes only the placeholder account label, preserving real account identities", () => {
+ expect(navAccountDisplayName(null, "default_user_id", "账户")).toBe("账户");
+ expect(navAccountDisplayName(null, "Account", "账户")).toBe("Account");
+ expect(navAccountDisplayName("admin@example.com", null, "账户")).toBe("admin@example.com");
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
index d6f51dc37a8..deb90ec53fb 100644
--- a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
+++ b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
@@ -1,15 +1,15 @@
/** Primary label for the navbar account control — avoids raw placeholder JWT/user IDs in the UI. */
-export function navAccountDisplayName(userEmail: string | null, userId: string | null): string {
+export function navAccountDisplayName(userEmail: string | null, userId: string | null, fallback = "Account"): string {
const email = userEmail?.trim();
if (email) {
return email;
}
const id = userId?.trim();
if (!id) {
- return "Account";
+ return fallback;
}
if (/^default[_\s-]?user[_\s-]?id$/i.test(id)) {
- return "Account";
+ return fallback;
}
return id;
}
diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
index 173e3b3f07e..e5072175971 100644
--- a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
+++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
@@ -98,6 +99,7 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
} = useAuthorized();
const router = useRouter();
const [open, setOpen] = React.useState(false);
+ const { t } = useTranslation();
const { data: healthData } = useHealthReadinessDetails(accessToken);
const version = healthData?.litellm_version;
const disableShowPrompts = useDisableShowPrompts();
@@ -119,29 +121,29 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
const toggles = [
{
key: "disableShowNewBadge",
- label: "Hide New Feature Indicators",
- ariaLabel: "Toggle hide new feature indicators",
+ label: t("Hide New Feature Indicators"),
+ ariaLabel: t("Toggle hide new feature indicators"),
checked: disableShowNewBadge,
onCheckedChange: (checked: boolean) => setFlag("disableShowNewBadge", checked),
},
{
key: "disableShowPrompts",
- label: "Hide All Prompts",
- ariaLabel: "Toggle hide all prompts",
+ label: t("Hide All Prompts"),
+ ariaLabel: t("Toggle hide all prompts"),
checked: disableShowPrompts,
onCheckedChange: (checked: boolean) => setFlag("disableShowPrompts", checked),
},
{
key: "disableBlogPosts",
- label: "Hide Blog Posts",
- ariaLabel: "Toggle hide blog posts",
+ label: t("Hide Blog Posts"),
+ ariaLabel: t("Toggle hide blog posts"),
checked: disableBlogPosts,
onCheckedChange: (checked: boolean) => setFlag("disableBlogPosts", checked),
},
{
key: "disableBouncingIcon",
- label: "Hide Bouncing Icon",
- ariaLabel: "Toggle hide bouncing icon",
+ label: t("Hide Bouncing Icon"),
+ ariaLabel: t("Toggle hide bouncing icon"),
checked: disableBouncingIcon,
onCheckedChange: (checked: boolean) => setFlag("disableBouncingIcon", checked),
},
@@ -150,8 +152,11 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
const seed = userEmail || userId || "user";
const initials = initialsFromIdentity(userEmail, userId);
const hue = hueFromString(seed);
- const displayName = navAccountDisplayName(userEmail, userId);
- const triggerLabel = `Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`;
+ const displayName = navAccountDisplayName(userEmail, userId, t("Account"));
+ const triggerLabel = t("accountMenu", {
+ role: userRole ?? t("Unknown role"),
+ identity: userEmail || userId || t("unknown"),
+ });
return (
@@ -192,7 +197,7 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
🌴
@@ -211,27 +216,27 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla