From 09b721ab2e134078e02bf5039922b77b6b06427d Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Nov 2026 12:42:59 -0800 Subject: [PATCH] refactor(ui): move remaining playground, chat and compliance text onto i18n (#50239) Compliance framework and category names, the Compare endpoint labels, the Decisions presets and schema messages, chat metrics, MCP events, reasoning toggle, conversation date groups, model filters and the MCP boolean argument labels now resolve through en.json. Ids, stored values and text sent to a model are unchanged --- .../components/compareUI/CompareUI.tsx | 6 +- .../components/UnifiedSelector.test.tsx | 9 +- .../compareUI/components/UnifiedSelector.tsx | 4 +- .../compareUI/endpoint_config.test.ts | 3 +- .../components/compareUI/endpoint_config.ts | 31 ++-- .../components/complianceUI/ComplianceUI.tsx | 30 +++- .../components/systemOneUI/SystemOneUI.tsx | 2 +- .../systemOneUI/lib/example.test.ts | 3 +- .../components/systemOneUI/lib/example.ts | 11 +- .../systemOneUI/lib/openAIDecisions.ts | 7 +- .../components/systemOneUI/lib/schemas.ts | 13 +- .../_components/pipeline_flow_builder.tsx | 10 +- .../src/components/chat/ConversationList.tsx | 10 +- .../components/chat_ui/MCPEventsDisplay.tsx | 16 +- .../components/chat_ui/ReasoningContent.tsx | 4 +- .../components/chat_ui/ResponseMetrics.tsx | 31 ++-- .../mcp_tools/MCPToolArgumentsForm.tsx | 17 +- .../src/components/model_filters.tsx | 34 ++-- .../src/data/compliancePrompts.ts | 45 +++++ ui/litellm-dashboard/src/i18n/locales/en.json | 154 ++++++++++++++++++ 20 files changed, 350 insertions(+), 90 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx index 1259f2daeae..e2fc05ada05 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx @@ -498,7 +498,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }: } // Validate selection based on endpoint type if (targetComparisons.some((comparison) => !hasValidSelection(comparison, selectedEndpoint))) { - toast.fromError(endpointConfig.validationMessage); + toast.fromError(t(endpointConfig.validationMessage)); return; } @@ -825,10 +825,10 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }: ) : isAnyComparisonLoading ? ( - {endpointConfig.loadingMessage} + {t(endpointConfig.loadingMessage)} ) : ( - {endpointConfig.inputPlaceholder} + {t(endpointConfig.inputPlaceholder)} )} {uploadedFile && ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx index d471a6ddaf8..a92ab1f2604 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; +import i18n from "@/i18n/i18n"; import { UnifiedSelector } from "./UnifiedSelector"; import { EndpointId, ENDPOINT_CONFIGS } from "../endpoint_config"; @@ -32,19 +33,19 @@ describe("UnifiedSelector", () => { it("prompts with the endpoint's own selector copy", () => { render(); - expect(promptIsVisible(CHAT.selectorPlaceholder)).toBe(true); + expect(promptIsVisible(i18n.t(CHAT.selectorPlaceholder))).toBe(true); }); it("prompts with the agent endpoint's copy when configured for agents", () => { render(); - expect(promptIsVisible(AGENTS.selectorPlaceholder)).toBe(true); + expect(promptIsVisible(i18n.t(AGENTS.selectorPlaceholder))).toBe(true); }); it("swaps the prompt for a loading message while options are in flight", () => { render(); - expect(promptIsVisible(`Loading ${CHAT.selectorLabel.toLowerCase()}s...`)).toBe(true); + expect(promptIsVisible(`Loading ${i18n.t(CHAT.selectorLabel).toLowerCase()}s...`)).toBe(true); }); it("reports the chosen option's value, not its label", async () => { @@ -116,6 +117,6 @@ describe("UnifiedSelector", () => { await openList(user); - expect(await screen.findByText(`No ${CHAT.selectorLabel.toLowerCase()}s available`)).toBeInTheDocument(); + expect(await screen.findByText(`No ${i18n.t(CHAT.selectorLabel).toLowerCase()}s available`)).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx index 07518c3b656..8e042b8e9e3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx @@ -29,7 +29,7 @@ const matchesQuery = (option: SelectorOption, query: string): boolean => export function UnifiedSelector({ value, options, loading, config, onChange }: UnifiedSelectorProps) { const { t } = useTranslation(); const selected = options.find((option) => option.value === value) ?? null; - const noun = config.selectorLabel.toLowerCase(); + const noun = t(config.selectorLabel).toLowerCase(); return ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts index 6463b2f9e5e..4172dcf6e4e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import i18n from "@/i18n/i18n"; import { EndpointId, ENDPOINT_CONFIGS, @@ -44,7 +45,7 @@ describe("endpoint_config", () => { const config = getEndpointConfig(EndpointId.CHAT_COMPLETIONS); expect(config.id).toBe(EndpointId.CHAT_COMPLETIONS); expect(config.selectorType).toBe("model"); - expect(config.selectorLabel).toBe("Model"); + expect(i18n.t(config.selectorLabel)).toBe("Model"); }); it("should check if endpoint is agent endpoint", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts index 2d1ebdaedd6..ab30e90ae28 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts @@ -3,6 +3,7 @@ * Add new endpoints here to extend the comparison functionality. */ +import type { ParseKeys } from "i18next"; import { Agent } from "../../llm_calls/fetch_agents"; // Endpoint identifiers @@ -23,11 +24,11 @@ export interface EndpointConfig { id: EndpointIdType; label: string; selectorType: SelectorType; - selectorLabel: string; - selectorPlaceholder: string; - inputPlaceholder: string; - loadingMessage: string; - validationMessage: string; + selectorLabel: ParseKeys; + selectorPlaceholder: ParseKeys; + inputPlaceholder: ParseKeys; + loadingMessage: ParseKeys; + validationMessage: ParseKeys; } // Endpoint configurations @@ -36,21 +37,21 @@ export const ENDPOINT_CONFIGS: Record = { id: EndpointId.CHAT_COMPLETIONS, label: "/v1/chat/completions", selectorType: "model", - selectorLabel: "Model", - selectorPlaceholder: "Select a model", - inputPlaceholder: "Send a prompt to compare models", - loadingMessage: "Gathering responses from all models...", - validationMessage: "Select a model before sending a message.", + selectorLabel: "playground.compare.endpoints.chatCompletions.selectorLabel", + selectorPlaceholder: "playground.compare.endpoints.chatCompletions.selectorPlaceholder", + inputPlaceholder: "playground.compare.endpoints.chatCompletions.inputPlaceholder", + loadingMessage: "playground.compare.endpoints.chatCompletions.loadingMessage", + validationMessage: "playground.compare.endpoints.chatCompletions.validationMessage", }, [EndpointId.A2A_AGENTS]: { id: EndpointId.A2A_AGENTS, label: "/a2a (Agents)", selectorType: "agent", - selectorLabel: "Agent", - selectorPlaceholder: "Select an agent", - inputPlaceholder: "Send a message to compare agents", - loadingMessage: "Gathering responses from all agents...", - validationMessage: "Select an agent before sending a message.", + selectorLabel: "playground.compare.endpoints.a2aAgents.selectorLabel", + selectorPlaceholder: "playground.compare.endpoints.a2aAgents.selectorPlaceholder", + inputPlaceholder: "playground.compare.endpoints.a2aAgents.inputPlaceholder", + loadingMessage: "playground.compare.endpoints.a2aAgents.loadingMessage", + validationMessage: "playground.compare.endpoints.a2aAgents.validationMessage", }, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx index 15e21176c48..b9eb313ed55 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx @@ -1,6 +1,8 @@ "use client"; import { + COMPLIANCE_CATEGORY_LABEL_KEYS, + COMPLIANCE_FRAMEWORK_LABEL_KEYS, getFrameworks, type ComplianceCategory, type ComplianceFramework, @@ -46,6 +48,7 @@ import { Upload, X, } from "lucide-react"; +import type { ParseKeys, TFunction } from "i18next"; import Papa from "papaparse"; import { Trans, useTranslation } from "react-i18next"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -67,6 +70,21 @@ const CATEGORY_ICON_MAP: Record> = { + ...COMPLIANCE_FRAMEWORK_LABEL_KEYS, + Custom: "playground.compliance.library.customFramework", + "CSV Upload": "playground.compliance.library.csvFramework", +}; + +const CATEGORY_LABEL_KEYS: Readonly> = { + ...COMPLIANCE_CATEGORY_LABEL_KEYS, + "Custom Prompts": "playground.compliance.library.customCategory", + "Uploaded Prompts": "playground.compliance.library.csvCategory", +}; + +const labelFor = (labelKeys: Readonly>, id: string, t: TFunction): string => + Object.hasOwn(labelKeys, id) ? t(labelKeys[id]) : id; + function CategoryIcon({ iconKey, className = "w-4 h-4 text-muted-foreground", @@ -1085,7 +1103,9 @@ export default function ComplianceUI({ )}
- {fw.name} + + {labelFor(FRAMEWORK_LABEL_KEYS, fw.name, t)} + {t("playground.compliance.library.promptCount", { total: fwPromptCount })} @@ -1137,7 +1157,7 @@ export default function ComplianceUI({ /> - {category.name} + {labelFor(CATEGORY_LABEL_KEYS, category.name, t)} {category.prompts.length} @@ -1153,7 +1173,9 @@ export default function ComplianceUI({

- {category.description} + {category.descriptionKey + ? term(t(category.descriptionKey)) + : category.description}

)} diff --git a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx index ad759c8dc67..daef8610867 100644 --- a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx +++ b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx @@ -19,6 +19,7 @@ import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { ScrollArea } from "@/components/ui/scroll-area"; import dayjs from "dayjs"; +import type { ParseKeys } from "i18next"; import { useTranslation } from "react-i18next"; import { Conversation } from "./types"; @@ -47,6 +48,13 @@ const getDateGroup = (timestamp: number): DateGroup => { const DATE_GROUP_ORDER: DateGroup[] = ["Recents", "Yesterday", "Last 7 Days", "Older"]; +const DATE_GROUP_LABEL_KEYS: Readonly> = { + Recents: "chat.conversationList.recent", + Yesterday: "chat.conversationList.yesterday", + "Last 7 Days": "chat.conversationList.lastSevenDays", + Older: "chat.conversationList.older", +}; + interface GroupedConversations { group: DateGroup; items: Conversation[]; @@ -350,7 +358,7 @@ const ConversationList: React.FC = ({
{items.map((conv) => ( >({}); const toggleKey = (key: string, open: boolean) => { @@ -72,7 +74,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) { {toolsEvent && ( toggleKey("list-tools", open)} > @@ -95,13 +97,15 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) { toggleKey(key, open)} >
-
Request
+
+ {t("chatUi.mcpEvents.request")} +
{callEvent.item?.arguments && (
@@ -116,13 +120,15 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
                     
-                    Approved
+                    {t("chatUi.mcpEvents.approved")}
                   
{callEvent.item?.output && (
-
Response
+
+ {t("chatUi.mcpEvents.response")} +
{callEvent.item.output}
diff --git a/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx b/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx index 73c952eab3a..c621c80ee64 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; @@ -13,6 +14,7 @@ interface ReasoningContentProps { } const ReasoningContent: React.FC = ({ reasoningContent }) => { + const { t } = useTranslation(); const syntaxTheme = useSyntaxTheme(coy); const [isExpanded, setIsExpanded] = useState(true); @@ -32,7 +34,7 @@ const ReasoningContent: React.FC = ({ reasoningContent }) } > - {isExpanded ? "Hide reasoning" : "Show reasoning"} + {isExpanded ? t("chatUi.reasoning.hide") : t("chatUi.reasoning.show")} {isExpanded ? : } diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx index abd0bacf0c9..f8cf9350309 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx @@ -101,6 +101,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) { } const ResponseMetrics: React.FC = ({ timeToFirstToken, totalLatency, usage, toolName }) => { + const { t } = useTranslation(); if (!timeToFirstToken && !totalLatency && !usage) return null; return ( @@ -108,7 +109,7 @@ const ResponseMetrics: React.FC = ({ timeToFirstToken, tot {timeToFirstToken !== undefined && (
@@ -226,10 +228,10 @@ const ModelFilters: React.FC = ({ const filtersContent = (
-

Search Models:

+

{t("modelFilters.searchModels")}

setSearchTerm(e.target.value)} className="border rounded-sm px-3 py-2 w-64 h-10 text-sm" @@ -237,14 +239,14 @@ const ModelFilters: React.FC = ({
{showProviderFilter && (
-

Provider:

+

{t("modelFilters.provider")}

changeMode(e.target.value)} className="border rounded-sm px-3 py-2 text-sm text-muted-foreground w-32 h-10" > {modelHubData && getUniqueModes(modelHubData).map((mode) => ( @@ -274,14 +276,14 @@ const ModelFilters: React.FC = ({
-

Features:

+

{t("modelFilters.features")}