From c3617bc3d27cf6b5a131662a0b0b8885b32bdc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B9=E9=9B=AA?= <2b@yorha.xyz> Date: Wed, 10 Jun 2026 18:45:29 +0900 Subject: [PATCH] feat(ui): i18n for mcp_tools batch 1 --- .../mcp_tools/ByokCredentialModal.tsx | 57 ++++---- .../components/mcp_tools/EnvVarsSection.tsx | 46 ++++--- .../components/mcp_tools/MCPLogoSelector.tsx | 10 +- .../mcp_tools/MCPNetworkSettings.tsx | 24 ++-- .../mcp_tools/MCPPermissionManagement.tsx | 78 ++++++----- .../components/mcp_tools/MCPServerCard.tsx | 47 ++++--- .../mcp_tools/MCPStandardsSettings.test.tsx | 27 ++-- .../mcp_tools/MCPStandardsSettings.tsx | 33 ++--- .../mcp_tools/MCPSubmissionsTab.tsx | 33 +++-- ui/litellm-dashboard/src/locales/en.json | 124 ++++++++++++++++++ ui/litellm-dashboard/src/locales/zh-CN.json | 124 ++++++++++++++++++ 11 files changed, 453 insertions(+), 150 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx index 84203a23950..876e42a29d7 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { Modal, Input, Switch } from "antd"; +import { useTranslation } from "react-i18next"; import MessageManager from "@/components/molecules/message_manager"; import { KeyOutlined, @@ -29,6 +30,7 @@ export const ByokCredentialModal: React.FC = ({ onSuccess, accessToken, }) => { + const { t } = useTranslation(); const [step, setStep] = useState<1 | 2>(1); const [apiKey, setApiKey] = useState(""); const [saveKey, setSaveKey] = useState(true); @@ -47,7 +49,7 @@ export const ByokCredentialModal: React.FC = ({ const handleAuthorize = async () => { if (!apiKey.trim()) { - MessageManager.error("Please enter your API key"); + MessageManager.error(t("mcpTools.byokCredentialModal.pleaseEnterApiKey")); return; } setLoading(true); @@ -64,11 +66,11 @@ export const ByokCredentialModal: React.FC = ({ const err = await response.json(); throw new Error(err?.detail?.error || "Failed to save credential"); } - MessageManager.success(`Connected to ${serverDisplayName}`); + MessageManager.success(t("mcpTools.byokCredentialModal.connectedSuccess", { name: serverDisplayName })); onSuccess(server.server_id); handleClose(); } catch (e: any) { - MessageManager.error(e.message || "Failed to connect"); + MessageManager.error(e.message || t("mcpTools.byokCredentialModal.failedToConnect")); } finally { setLoading(false); } @@ -84,7 +86,7 @@ export const ByokCredentialModal: React.FC = ({ onClick={() => setStep(1)} className="flex items-center gap-1 text-gray-500 hover:text-gray-800 text-sm" > - Back + {t("common.back")} ) : (
@@ -111,8 +113,12 @@ export const ByokCredentialModal: React.FC = ({
-

Connect {serverDisplayName}

-

LiteLLM needs access to {serverDisplayName} to complete your request.

+

+ {t("mcpTools.byokCredentialModal.connectTitle", { name: serverDisplayName })} +

+

+ {t("mcpTools.byokCredentialModal.connectDesc", { name: serverDisplayName })} +

{/* How it works */}
@@ -124,10 +130,11 @@ export const ByokCredentialModal: React.FC = ({
-

How it works

+

+ {t("mcpTools.byokCredentialModal.howItWorksTitle")} +

- LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to{" "} - {serverDisplayName}'s API. + {t("mcpTools.byokCredentialModal.howItWorksDesc", { name: serverDisplayName })}

@@ -141,7 +148,7 @@ export const ByokCredentialModal: React.FC = ({ - Requested Access + {t("mcpTools.byokCredentialModal.requestedAccess")}

    {server.byok_description.map((item, i) => ( @@ -158,10 +165,10 @@ export const ByokCredentialModal: React.FC = ({ onClick={() => setStep(2)} className="w-full bg-gray-900 hover:bg-gray-700 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors" > - Continue to Authentication + {t("mcpTools.byokCredentialModal.continueToAuth")} ) : ( @@ -171,13 +178,19 @@ export const ByokCredentialModal: React.FC = ({ -

    Provide API Key

    -

    Enter your {serverDisplayName} API key to authorize this connection.

    +

    + {t("mcpTools.byokCredentialModal.provideApiKeyTitle")} +

    +

    + {t("mcpTools.byokCredentialModal.provideApiKeyDesc", { name: serverDisplayName })} +

    - + setApiKey(e.target.value)} size="large" @@ -190,7 +203,7 @@ export const ByokCredentialModal: React.FC = ({ rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700 text-sm mt-2 flex items-center gap-1" > - Where do I find my API key? + {t("mcpTools.byokCredentialModal.whereIsMyApiKey")} )}
    @@ -204,7 +217,9 @@ export const ByokCredentialModal: React.FC = ({ fill="currentColor" /> - Save key for future use + + {t("mcpTools.byokCredentialModal.saveKeyLabel")} + @@ -212,9 +227,7 @@ export const ByokCredentialModal: React.FC = ({ {/* Security note */}
    -

    - Your key is stored securely and transmitted over HTTPS. It is never shared with third parties. -

    +

    {t("mcpTools.byokCredentialModal.securityNote")}

    )} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx b/ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx index 4a694d71585..6683168358c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx @@ -1,12 +1,14 @@ import React from "react"; import { Form, Input, Select, Button, Tooltip, Typography } from "antd"; import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; const { Text } = Typography; -const SCOPE_OPTIONS = [ - { value: "global", label: "Instance" }, - { value: "user", label: "Per-user" }, +export const getScopeOptions = (t: TFunction) => [ + { value: "global", label: t("mcpTools.envVarsSection.scopeInstance") }, + { value: "user", label: t("mcpTools.envVarsSection.scopePerUser") }, ]; /** @@ -20,21 +22,21 @@ const SCOPE_OPTIONS = [ * The parent form reads the ``env_vars`` field from the form values. */ const EnvVarsSection: React.FC = () => { + const { t } = useTranslation(); + const scopeOptions = React.useMemo(() => getScopeOptions(t), [t]); return (
    - Variables + {t("mcpTools.envVarsSection.sectionTitle")} - Define variables you can interpolate in Static Headers or Authentication using{" "} - {"${VAR_NAME}"}.
    - Instance: admin-defined value used for every user. + {t("mcpTools.envVarsSection.tooltipDefine")} {"${VAR_NAME}"}.
    + {t("mcpTools.envVarsSection.scopeInstance")}: {t("mcpTools.envVarsSection.tooltipInstance")}
    - Per-user: each user supplies their own value (e.g. personal credentials) via the MCP Gateway - dashboard. + {t("mcpTools.envVarsSection.scopePerUser")}: {t("mcpTools.envVarsSection.tooltipPerUser")} } > @@ -42,7 +44,8 @@ const EnvVarsSection: React.FC = () => {
    - Reference these in Static Headers or Authentication as {"${VAR_NAME}"}. For example:{" "} + {t("mcpTools.envVarsSection.referenceHint")} {"${VAR_NAME}"}.{" "} + {t("mcpTools.envVarsSection.exampleLabel")}{" "} {"${DB_PROTOCOL}://${CORP_USERNAME}:${CORP_PASSWORD}@${DB_HOSTNAME}"} @@ -53,9 +56,9 @@ const EnvVarsSection: React.FC = () => {
    {fields.length > 0 && (
    -
    Variable Name
    -
    Value / Description
    -
    Scope
    +
    {t("mcpTools.envVarsSection.colVarName")}
    +
    {t("mcpTools.envVarsSection.colValueDesc")}
    +
    {t("mcpTools.envVarsSection.colScope")}
    )} @@ -67,10 +70,10 @@ const EnvVarsSection: React.FC = () => { className="mb-0" style={{ flex: 1 }} rules={[ - { required: true, message: "Variable name is required" }, + { required: true, message: t("mcpTools.envVarsSection.varNameRequired") }, { pattern: /^[A-Za-z_][A-Za-z0-9_]*$/, - message: "Use letters, digits, underscores; cannot start with a digit.", + message: t("mcpTools.envVarsSection.varNamePattern"), }, ]} > @@ -86,7 +89,7 @@ const EnvVarsSection: React.FC = () => { initialValue="global" style={{ width: 160 }} > -
    {
    ))}
    )} @@ -113,20 +116,21 @@ const ScopedValueOrDescription: React.FC<{ name: number; restField: object; }> = ({ name, restField }) => { + const { t } = useTranslation(); const isPerUser = Form.useWatch(["env_vars", name, "scope"]) === "user"; if (isPerUser) { return ( + - Hint + {t("mcpTools.envVarsSection.hintLabel")} } - placeholder="e.g. Your DB username" + placeholder={t("mcpTools.envVarsSection.perUserPlaceholder")} styles={{ input: { color: "#9ca3af" } }} /> @@ -134,7 +138,7 @@ const ScopedValueOrDescription: React.FC<{ } return ( - + ); }; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx index eaf1b86a783..03b152b9c44 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { Input, Tooltip } from "antd"; import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; const logos = "/ui/assets/logos/"; @@ -33,6 +34,7 @@ interface MCPLogoSelectorProps { } const MCPLogoSelector: React.FC = ({ value, onChange }) => { + const { t } = useTranslation(); const [imgErrors, setImgErrors] = useState>(new Set()); const handleSelect = (url: string) => { @@ -46,8 +48,8 @@ const MCPLogoSelector: React.FC = ({ value, onChange }) => return (
    - Logo - + {t("mcpTools.mcpLogoSelector.logoLabel")} +
    @@ -57,7 +59,7 @@ const MCPLogoSelector: React.FC = ({ value, onChange }) =>
    Selected logo { (e.target as HTMLImageElement).style.display = "none"; @@ -110,7 +112,7 @@ const MCPLogoSelector: React.FC = ({ value, onChange }) => {/* Custom URL input */} } - placeholder="Or paste a custom logo URL..." + placeholder={t("mcpTools.mcpLogoSelector.customUrlPlaceholder")} value={value && !WELL_KNOWN_LOGOS.some((l) => l.url === value) ? value : ""} onChange={(e) => { const v = e.target.value.trim(); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx index 2fc9e91e0c1..9f686fe8532 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { Select, Button, Card, Typography, Spin, Tag } from "antd"; import { SaveOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; import { getGeneralSettingsCall, updateConfigFieldSetting, @@ -24,6 +25,7 @@ function ipToSlash24(ip: string): string { } const MCPNetworkSettings: React.FC = ({ accessToken }) => { + const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [privateRanges, setPrivateRanges] = useState([]); @@ -94,22 +96,20 @@ const MCPNetworkSettings: React.FC = ({ accessToken }) return (
    - Private IP Ranges -

    - Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. - Callers from any other IP can only see servers marked "Available on Public Internet". -

    + {t("mcpTools.mcpNetworkSettings.privateIpRangesTitle")} +

    {t("mcpTools.mcpNetworkSettings.privateIpRangesDesc")}

    {currentIp && (
    - Your current IP: {currentIp} + {t("mcpTools.mcpNetworkSettings.yourCurrentIp")}{" "} + {currentIp} {suggestedRange && !privateRanges.includes(suggestedRange) && (
    - Suggested range: + {t("mcpTools.mcpNetworkSettings.suggestedRange")} = ({ accessToken }) )}
    - Your Private Network Ranges + {t("mcpTools.mcpNetworkSettings.yourPrivateNetworkRanges")}
    (option?.value ?? "").toLowerCase().includes(input.toLowerCase())} onSearch={(value) => setSearchValue(value)} @@ -252,13 +251,13 @@ const MCPPermissionManagement: React.FC = ({ - Extra Headers - + {t("mcpTools.mcpPermissionManagement.extraHeadersLabel")} + {mcpServer?.extra_headers && mcpServer.extra_headers.length > 0 && ( - {mcpServer.extra_headers.length} configured + {t("mcpTools.mcpPermissionManagement.configuredCount", { count: mcpServer.extra_headers.length })} )} @@ -269,8 +268,10 @@ const MCPPermissionManagement: React.FC = ({ mode="tags" placeholder={ mcpServer?.extra_headers && mcpServer.extra_headers.length > 0 - ? `Currently: ${mcpServer.extra_headers.join(", ")}` - : "Enter header names (e.g., Authorization, X-Custom-Header)" + ? t("mcpTools.mcpPermissionManagement.extraHeadersCurrently", { + headers: mcpServer.extra_headers.join(", "), + }) + : t("mcpTools.mcpPermissionManagement.extraHeadersPlaceholder") } className="rounded-lg" size="large" @@ -282,8 +283,8 @@ const MCPPermissionManagement: React.FC = ({ - Static Headers - + {t("mcpTools.mcpPermissionManagement.staticHeadersLabel")} + @@ -299,22 +300,27 @@ const MCPPermissionManagement: React.FC = ({ {...restField} name={[name, "header"]} className="flex-1" - rules={[{ required: true, message: "Header name is required" }]} + rules={[{ required: true, message: t("mcpTools.mcpPermissionManagement.headerNameRequired") }]} > - + remove(name)} @@ -323,7 +329,7 @@ const MCPPermissionManagement: React.FC = ({ ))}
    )} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx index 064a9481325..4d47ab2084a 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx @@ -1,6 +1,7 @@ import { useState, type FC, type KeyboardEvent, type MouseEvent } from "react"; import { Dropdown, Tooltip, Typography, Tag } from "antd"; import type { MenuProps } from "antd"; +import { useTranslation } from "react-i18next"; import { CheckOutlined, DeleteOutlined, @@ -48,6 +49,7 @@ const MCPServerCard: FC = ({ onOpenFillFields, onDelete, }) => { + const { t } = useTranslation(); const alias = server.alias || server.server_name || ""; const name = server.server_name || alias || server.server_id; // Logo is sourced exclusively from the admin-set `mcp_info.logo_url`. @@ -102,7 +104,7 @@ const MCPServerCard: FC = ({ if (onRecheckHealth) { menuItems.push({ key: "test-connection", - label: "Test Connection", + label: t("mcpTools.mcpServerCard.testConnection"), icon: , disabled: isRechecking, onClick: ({ domEvent }) => { @@ -117,7 +119,7 @@ const MCPServerCard: FC = ({ } menuItems.push({ key: "delete", - label: "Delete", + label: t("common.delete"), icon: , danger: true, onClick: ({ domEvent }) => { @@ -143,7 +145,7 @@ const MCPServerCard: FC = ({ {logoUrl ? ( {`${name} setFailedLogoUrl(logoUrl)} /> @@ -170,7 +172,7 @@ const MCPServerCard: FC = ({ type="button" onClick={stop} onKeyDown={stop} - aria-label="Server actions" + aria-label={t("mcpTools.mcpServerCard.serverActionsAriaLabel")} className="-mr-1 -mt-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600" > @@ -206,7 +208,7 @@ const MCPServerCard: FC = ({ - {isPublic ? "Public" : "Internal"} + {isPublic ? t("mcpTools.mcpServerCard.public") : t("mcpTools.mcpServerCard.internal")} {accessGroups.slice(0, 2).map((g) => ( @@ -229,7 +231,7 @@ const MCPServerCard: FC = ({ -
    Missing user fields:
    +
    {t("mcpTools.mcpServerCard.missingUserFieldsTitle")}
      {missing.map((m) => (
    • • {m}
    • @@ -240,8 +242,7 @@ const MCPServerCard: FC = ({ > - {missing.length} user field - {missing.length === 1 ? "" : "s"} missing + {t("mcpTools.mcpServerCard.missingUserFields", { count: missing.length })} {onOpenFillFields && ( @@ -253,7 +254,7 @@ const MCPServerCard: FC = ({ }} className="rounded-md bg-red-600 px-3 py-1 text-xs font-medium text-white shadow-sm transition-colors hover:bg-red-700" > - Set + {t("mcpTools.mcpServerCard.setButton")} )}
    @@ -283,28 +284,33 @@ const HealthChip: FC = ({ error, dotClass, }) => { + const { t } = useTranslation(); if (isLoadingHealth || isRechecking) { return ( - Checking + {t("mcpTools.mcpServerCard.checking")} ); } const tooltip = (
    -
    Health: {status}
    - {lastCheck &&
    Last check: {new Date(lastCheck).toLocaleString()}
    } +
    {t("mcpTools.mcpServerCard.healthLabel", { status })}
    + {lastCheck && ( +
    + {t("mcpTools.mcpServerCard.lastCheck", { time: new Date(lastCheck).toLocaleString() })} +
    + )} {error && (
    -
    Error
    +
    {t("common.error")}
    {error}
    )} - {!lastCheck && !error &&
    No health data
    } - {onRecheck &&
    Click to recheck
    } + {!lastCheck && !error &&
    {t("mcpTools.mcpServerCard.noHealthData")}
    } + {onRecheck &&
    {t("mcpTools.mcpServerCard.clickToRecheck")}
    }
    ); return ( @@ -335,13 +341,14 @@ interface ByokRowProps { } const ByokRow: FC = ({ connected, onConnect }) => { + const { t } = useTranslation(); if (connected) { return (
    - BYOK credential + {t("mcpTools.mcpServerCard.byokCredential")}
    - Connected + {t("mcpTools.mcpServerCard.connected")} {onConnect && ( )}
    @@ -361,7 +368,7 @@ const ByokRow: FC = ({ connected, onConnect }) => { } return (
    - BYOK credential + {t("mcpTools.mcpServerCard.byokCredential")} {onConnect ? ( ) : ( — diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx index fd8d9f92c48..62acb219b77 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx @@ -1,7 +1,16 @@ import { describe, it, expect } from "vitest"; -import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings"; +import { + getFieldGroups, + getMcpRequiredFieldDefs, + SETTINGS_KEY, + FieldGroup, + RequiredFieldDef, +} from "./MCPStandardsSettings"; +import type { TFunction } from "i18next"; import { MCPServer } from "./types"; +const t = ((key: string) => key) as unknown as TFunction; + const makeServer = (overrides: Partial = {}): MCPServer => ({ server_id: "s1", created_at: "2024-01-01", @@ -11,22 +20,24 @@ const makeServer = (overrides: Partial = {}): MCPServer => ({ ...overrides, }); -describe("FIELD_GROUPS", () => { +describe("getFieldGroups", () => { it("should contain four groups", () => { - expect(FIELD_GROUPS).toHaveLength(4); - expect(FIELD_GROUPS.map((g) => g.label)).toEqual(["Documentation", "Source", "Connection", "Security"]); + const groups = getFieldGroups(t); + expect(groups).toHaveLength(4); }); }); -describe("MCP_REQUIRED_FIELD_DEFS", () => { +describe("getMcpRequiredFieldDefs", () => { it("should flatten all fields from groups", () => { - const totalFields = FIELD_GROUPS.reduce((sum, g) => sum + g.fields.length, 0); - expect(MCP_REQUIRED_FIELD_DEFS).toHaveLength(totalFields); + const groups = getFieldGroups(t); + const defs = getMcpRequiredFieldDefs(t); + const totalFields = groups.reduce((sum: number, g: FieldGroup) => sum + g.fields.length, 0); + expect(defs).toHaveLength(totalFields); }); }); describe("field check functions", () => { - const findCheck = (key: string) => MCP_REQUIRED_FIELD_DEFS.find((f) => f.key === key)!.check; + const findCheck = (key: string) => getMcpRequiredFieldDefs(t).find((f: RequiredFieldDef) => f.key === key)!.check; it("should pass description check when description is present", () => { expect(findCheck("description")(makeServer({ description: "A service" }))).toBe(true); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx index fb38e392631..dabc2e90976 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx @@ -1,5 +1,6 @@ "use client"; +import type { TFunction } from "i18next"; import { MCPServer } from "./types"; export interface RequiredFieldDef { @@ -14,59 +15,59 @@ export interface FieldGroup { fields: RequiredFieldDef[]; } -export const FIELD_GROUPS: FieldGroup[] = [ +export const getFieldGroups = (t: TFunction): FieldGroup[] => [ { - label: "Documentation", + label: t("mcpTools.mcpStandardsSettings.groupDocumentation"), fields: [ { key: "description", - label: "Description", - description: "Must have a non-empty description", + label: t("mcpTools.mcpStandardsSettings.fieldDescriptionLabel"), + description: t("mcpTools.mcpStandardsSettings.fieldDescriptionDesc"), check: (s) => !!s.description?.trim(), }, { key: "alias", - label: "Alias", - description: "Must have a display alias", + label: t("mcpTools.mcpStandardsSettings.fieldAliasLabel"), + description: t("mcpTools.mcpStandardsSettings.fieldAliasDesc"), check: (s) => !!s.alias?.trim(), }, ], }, { - label: "Source", + label: t("mcpTools.mcpStandardsSettings.groupSource"), fields: [ { key: "source_url", - label: "GitHub / Source URL", - description: "Must link to a source repository", + label: t("mcpTools.mcpStandardsSettings.fieldSourceUrlLabel"), + description: t("mcpTools.mcpStandardsSettings.fieldSourceUrlDesc"), check: (s) => !!s.source_url?.trim(), }, ], }, { - label: "Connection", + label: t("mcpTools.mcpStandardsSettings.groupConnection"), fields: [ { key: "url", - label: "Server URL", - description: "Must have a URL configured", + label: t("mcpTools.mcpStandardsSettings.fieldServerUrlLabel"), + description: t("mcpTools.mcpStandardsSettings.fieldServerUrlDesc"), check: (s) => !!s.url?.trim(), }, ], }, { - label: "Security", + label: t("mcpTools.mcpStandardsSettings.groupSecurity"), fields: [ { key: "auth_type", - label: "Auth configured", - description: "Must use authentication (not 'none')", + label: t("mcpTools.mcpStandardsSettings.fieldAuthLabel"), + description: t("mcpTools.mcpStandardsSettings.fieldAuthDesc"), check: (s) => !!s.auth_type && s.auth_type !== "none", }, ], }, ]; -export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = FIELD_GROUPS.flatMap((g) => g.fields); +export const getMcpRequiredFieldDefs = (t: TFunction): RequiredFieldDef[] => getFieldGroups(t).flatMap((g) => g.fields); export const SETTINGS_KEY = "mcp_required_fields"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx index fc0a0779b24..5395c888f74 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx @@ -19,8 +19,15 @@ import { updateConfigFieldSetting, } from "@/components/networking"; import { MCPServer, MCPSubmissionsSummary } from "./types"; -import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings"; +import { + getFieldGroups, + getMcpRequiredFieldDefs, + FieldGroup, + RequiredFieldDef, + SETTINGS_KEY, +} from "./MCPStandardsSettings"; import NotificationsManager from "@/components/molecules/notifications_manager"; +import { useTranslation } from "react-i18next"; type MCPStatus = "active" | "pending_review" | "rejected"; @@ -139,8 +146,11 @@ type SubmissionRulesPanelProps = { }; function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: SubmissionRulesPanelProps) { + const { t } = useTranslation(); + const fieldDefs = getMcpRequiredFieldDefs(t); + const fieldGroups = getFieldGroups(t); const [expanded, setExpanded] = useState(false); - const activeLabels = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key)); + const activeLabels = fieldDefs.filter((f: RequiredFieldDef) => requiredFields.includes(f.key)); const toggle = (key: string) => { onChange(requiredFields.includes(key) ? requiredFields.filter((k) => k !== key) : [...requiredFields, key]); @@ -195,11 +205,11 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su for each rule on every submission card below.

    - {FIELD_GROUPS.map((group) => ( + {fieldGroups.map((group: FieldGroup) => (
    {group.label}
    - {group.fields.map((field) => { + {group.fields.map((field: RequiredFieldDef) => { const active = requiredFields.includes(field.key); return (