From caf77c841927d66e2e2a5aff0282db3d29b73bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B9=E9=9B=AA?= <2b@yorha.xyz> Date: Thu, 11 Jun 2026 04:12:45 +0900 Subject: [PATCH] feat(ui): i18n for chat conversation list and MCP panels --- .../src/components/chat/ConversationList.tsx | 46 +++++++----- .../src/components/chat/MCPAppsPanel.tsx | 73 +++++++++++-------- .../src/components/chat/MCPConnectPicker.tsx | 10 ++- .../src/components/chat/MCPCredentialsTab.tsx | 57 +++++++++------ ui/litellm-dashboard/src/locales/en.json | 66 +++++++++++++++++ ui/litellm-dashboard/src/locales/zh-CN.json | 66 +++++++++++++++++ 6 files changed, 244 insertions(+), 74 deletions(-) diff --git a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx index f75ee8fa40a..7243ac1be78 100644 --- a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx +++ b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx @@ -11,6 +11,7 @@ import { MessageOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; import { Conversation } from "./types"; const { Text } = Typography; @@ -71,6 +72,7 @@ interface ConversationRowProps { } const ConversationRow: React.FC = ({ conv, isActive, onSelect, onDelete, onRename }) => { + const { t } = useTranslation(); const [editing, setEditing] = useState(false); const [editValue, setEditValue] = useState(conv.title); const inputRef = useRef(null); @@ -181,7 +183,7 @@ const ConversationRow: React.FC = ({ conv, isActive, onSel }} onClick={(e) => e.stopPropagation()} > - + @@ -371,11 +384,10 @@ const ConversationList: React.FC = ({ fontSize: 12, marginTop: 32, padding: "0 12px", + whiteSpace: "pre-line", }} > - No conversations yet. -
- Start a new chat above. + {t("chat.conversationList.noConversationsYet")} ) : ( grouped.map(({ group, items }) => ( @@ -390,7 +402,7 @@ const ConversationList: React.FC = ({ padding: "8px 8px 4px", }} > - {group} + {dateGroupLabel(group)} {items.map((conv) => ( = ({ textOverflow: "ellipsis", }} > - My Account + {t("chat.conversationList.myAccount")} diff --git a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx index 880d408eb33..f7f8f03623b 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { Spin, Input, Button, Skeleton } from "antd"; import { SearchOutlined, ArrowLeftOutlined, RightOutlined, ToolOutlined, CheckCircleOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; import { deleteMCPOAuthUserCredential, fetchMCPServers, @@ -29,6 +30,7 @@ const OAuth2ConnectButton: React.FC = ({ onConnect, variant = "badge", }) => { + const { t } = useTranslation(); const name = server.server_name ?? server.alias ?? server.server_id; const { startOAuthFlow, status } = useUserMcpOAuthFlow({ accessToken, @@ -47,7 +49,7 @@ const OAuth2ConnectButton: React.FC = ({ onClick={startOAuthFlow} style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }} > - {loading ? "Connecting…" : "Connect"} + {loading ? t("chat.mCPAppsPanel.connecting") : t("chat.mCPAppsPanel.connect")} ); } @@ -70,7 +72,7 @@ const OAuth2ConnectButton: React.FC = ({ whiteSpace: "nowrap", }} > - {loading ? "Connecting…" : "Connect"} + {loading ? t("chat.mCPAppsPanel.connecting") : t("chat.mCPAppsPanel.connect")} ); }; @@ -104,6 +106,7 @@ function getAvatarColor(name: string): string { type TabKey = "all" | "connected"; const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange }) => { + const { t } = useTranslation(); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [query, setQuery] = useState(""); @@ -228,7 +231,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange const idToFetch = serverId ?? serverName; const result = await listMCPTools(accessToken, idToFetch); if (result?.error) { - MessageManager.warning(`Could not load tools for ${serverName}`); + MessageManager.warning(t("chat.mCPAppsPanel.couldNotLoadTools", { name: serverName })); return; } // Use the ref so we read the most up-to-date list; guard against duplicates @@ -237,7 +240,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange onChange([...selectedServersRef.current, serverName]); } } catch { - MessageManager.warning(`Could not load tools for ${serverName}`); + MessageManager.warning(t("chat.mCPAppsPanel.couldNotLoadTools", { name: serverName })); } finally { setTogglingOn((prev) => { const next = new Set(prev); @@ -313,7 +316,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange }} > - Back + {t("common.back")} {/* Avatar + name + connect */} @@ -321,7 +324,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange {detailServer.mcp_info?.logo_url ? ( {`${name} = ({ accessToken, selectedServers, onChange

{name}

-

{detailServer.description ?? "MCP server"}

+

+ {detailServer.description ?? t("chat.mCPAppsPanel.mcpServerFallbackDesc")} +

{detailServer.auth_type === AUTH_TYPE.OAUTH2 ? ( oauthConnected.has(detailServer.server_id) ? ( @@ -378,7 +383,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange }} style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }} > - Disconnect + {t("chat.mCPAppsPanel.disconnect")} ) : ( = ({ accessToken, selectedServers, onChange onClick={() => handleToggle(name, !isConnected, detailServer.server_id)} style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }} > - {isConnected ? "Disconnect" : "Connect"} + {isConnected ? t("chat.mCPAppsPanel.disconnect") : t("chat.mCPAppsPanel.connect")} )} {/* Info table */} -

Information

+

+ {t("chat.mCPAppsPanel.information")} +

{[ - ["Server ID", detailServer.server_id], - ["Transport", handleTransport(detailServer.transport, detailServer.spec_path)], - ["Status", isConnected ? "Connected" : "Not connected"], + [t("chat.mCPAppsPanel.serverIdLabel"), detailServer.server_id], + [t("chat.mCPAppsPanel.transportLabel"), handleTransport(detailServer.transport, detailServer.spec_path)], + [t("common.status"), isConnected ? t("chat.mCPAppsPanel.connected") : t("chat.mCPAppsPanel.notConnected")], ] .filter(([, v]) => v) .map(([label, value], i, arr) => ( @@ -429,7 +436,9 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange {/* Tools section */}
-

Available Tools

+

+ {t("chat.mCPAppsPanel.availableTools")} +

{!loadingTools && ( = ({ accessToken, selectedServers, onChange
) : detailTools.length === 0 ? ( -
No tools available
+
+ {t("chat.mCPAppsPanel.noToolsAvailable")} +
) : (
{detailTools.map((tool) => ( @@ -496,7 +507,9 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange >
-

MCP Servers

+

+ {t("chat.mCPAppsPanel.mcpServersTitle")} +

= ({ accessToken, selectedServers, onChange textTransform: "uppercase", }} > - Beta + {t("chat.mCPAppsPanel.beta")}
-

- Browse tools, authenticate once, use in chat — no setup needed. -

+

{t("chat.mCPAppsPanel.browsePitch")}

{loadingCounts ? ( - Loading tools... + {t("chat.mCPAppsPanel.loadingTools")} ) : totalTools > 0 ? ( - {totalTools} tool{totalTools !== 1 ? "s" : ""} available + {t("chat.mCPAppsPanel.toolsAvailable", { count: totalTools })} ) : null}
} - placeholder="Search servers..." + placeholder={t("chat.mCPAppsPanel.searchServersPlaceholder")} value={query} onChange={(e) => setQuery(e.target.value)} allowClear @@ -558,7 +569,11 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange marginBottom: -1, }} > - {tab === "all" ? "All" : `Connected${connectedCount > 0 ? ` (${connectedCount})` : ""}`} + {tab === "all" + ? t("chat.mCPAppsPanel.tabAll") + : connectedCount > 0 + ? t("chat.mCPAppsPanel.tabConnectedCount", { count: connectedCount }) + : t("chat.mCPAppsPanel.tabConnected")} ))}
@@ -571,10 +586,10 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange ) : filtered.length === 0 ? (
{servers.length === 0 - ? "No MCP servers configured. Add servers in Tools → MCP Servers." + ? t("chat.mCPAppsPanel.noServersConfigured") : activeTab === "connected" - ? "No servers connected yet." - : "No servers match your search."} + ? t("chat.mCPAppsPanel.noServersConnected") + : t("chat.mCPAppsPanel.noServersMatch")}
) : (
= ({ accessToken, selectedServers, onChange {server.mcp_info?.logo_url ? ( {`${name} = ({ accessToken, selectedServers, onChange }} > - {server.description ?? "MCP server"} + {server.description ?? t("chat.mCPAppsPanel.mcpServerFallbackDesc")} {count !== undefined ? ( count > 0 ? ( diff --git a/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx b/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx index 3e13ec9dc87..5673affd651 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { Switch, Spin } from "antd"; +import { useTranslation } from "react-i18next"; import MessageManager from "@/components/molecules/message_manager"; import { fetchMCPServers, listMCPTools } from "../networking"; import { MCPServer } from "../mcp_tools/types"; @@ -11,6 +12,7 @@ interface Props { } const MCPConnectPicker: React.FC = ({ accessToken, selectedServers, onChange }) => { + const { t } = useTranslation(); const [servers, setServers] = useState([]); const [loadingServers, setLoadingServers] = useState(true); // Track which individual servers are being toggled on (verifying tools) @@ -58,13 +60,13 @@ const MCPConnectPicker: React.FC = ({ accessToken, selectedServers, onCha const result = await listMCPTools(accessToken, serverName); // listMCPTools never throws; it returns { tools, error, message } on failure if (result?.error) { - MessageManager.warning(`Could not load tools for ${serverName} — it will be excluded from this message.`); + MessageManager.warning(t("chat.mCPConnectPicker.couldNotLoadTools", { name: serverName })); // Do not add to selectedServers return; } onChange([...selectedServers, serverName]); } catch { - MessageManager.warning(`Could not load tools for ${serverName} — it will be excluded from this message.`); + MessageManager.warning(t("chat.mCPConnectPicker.couldNotLoadTools", { name: serverName })); // Do not add to selectedServers } finally { setTogglingOn((prev) => { @@ -90,7 +92,7 @@ const MCPConnectPicker: React.FC = ({ accessToken, selectedServers, onCha
) : servers.length === 0 ? (
- No MCP servers configured + {t("chat.mCPConnectPicker.noServersConfigured")}
) : ( servers.map((server) => { @@ -112,7 +114,7 @@ const MCPConnectPicker: React.FC = ({ accessToken, selectedServers, onCha {server.mcp_info?.logo_url && ( {`${name} 0) return `Expires in ${diffDay}d`; - if (diffHr > 0) return `Expires in ${diffHr}h`; - return `Expires in ${diffMin}m`; + if (diffDay > 0) return t("chat.mCPCredentialsTab.expiresInDays", { count: diffDay }); + if (diffHr > 0) return t("chat.mCPCredentialsTab.expiresInHours", { count: diffHr }); + return t("chat.mCPCredentialsTab.expiresInMinutes", { count: diffMin }); } catch { return ""; } } const MCPCredentialsTab: React.FC = ({ accessToken }) => { + const { t } = useTranslation(); const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(true); const [revoking, setRevoking] = useState>(new Set()); @@ -76,7 +79,7 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => { await deleteMCPOAuthUserCredential(accessToken, serverId); setCredentials((prev) => prev.filter((c) => c.server_id !== serverId)); } catch { - MessageManager.error("Failed to revoke connection. Please try again."); + MessageManager.error(t("chat.mCPCredentialsTab.revokeError")); } finally { setRevoking((prev) => { const n = new Set(prev); @@ -92,8 +95,8 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => {
{/* Header */}
-

App Credentials

-

Your stored OAuth connections — used automatically in chat.

+

{t("chat.mCPCredentialsTab.appCredentials")}

+

{t("chat.mCPCredentialsTab.oauthConnectionsDesc")}

{loading ? ( @@ -103,20 +106,26 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => { ) : credentials.length === 0 ? (
- No connections yet. + {t("chat.mCPCredentialsTab.noConnectionsYet")}
- Go to Apps and click Connect to authorize an MCP server. + }} />
) : (
- App - Connected - Status + + {t("chat.mCPCredentialsTab.colApp")} + + + {t("chat.mCPCredentialsTab.colConnected")} + + + {t("common.status")} + - Actions + {t("common.actions")} @@ -124,9 +133,9 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => { {credentials.map((cred) => { const name = displayName(cred); const isRevoking = revoking.has(cred.server_id); - const exp = expiryLabel(cred.expires_at); - const connected = relativeTime(cred.connected_at); - const isExpired = exp === "Expired"; + const exp = expiryLabel(cred.expires_at, t); + const connected = relativeTime(cred.connected_at, t); + const isExpired = exp === t("chat.mCPCredentialsTab.expired"); return ( @@ -145,7 +154,7 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => {