From 4eec9d0de43678fb99059bd216597677845b69e8 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Sat, 31 Oct 2026 15:13:59 -0700 Subject: [PATCH] refactor(ui): move agents and knowledge text onto i18n Move the page-authored English on the agents, vector stores, prompts, search tools, memory and skills pages, plus the shared skill detail, make-skill-public and model selector components, into en.json behind t(). The English on screen is unchanged. Keys are added under agents, vectorStores, prompts, searchTools, memory, skills, claudeCodePlugins and modelSelector, inserted at the top of en.json Strings that could only move with a structural change stay English, along with API values, code samples, paths and names --- .../agents/_components/AgentFormKit.tsx | 5 +- .../_components/AgentIdentityDetails.tsx | 50 +- .../_components/AgentIdentityFields.tsx | 143 +- .../_components/AgentKillSwitchDangerZone.tsx | 44 +- .../agents/_components/AgentsPanel.tsx | 31 +- .../agents/_components/AgentsTable.tsx | 25 +- .../agents/_components/AgentsTableColumns.tsx | 42 +- .../_components/KillSwitchFormFields.tsx | 74 +- .../agents/_components/add_agent_form.tsx | 258 ++- .../_components/agent_card_discovery.tsx | 84 +- .../agents/_components/agent_config.ts | 42 +- .../agents/_components/agent_cost_view.tsx | 10 +- .../agents/_components/agent_form_fields.tsx | 110 +- .../agents/_components/agent_info.tsx | 168 +- .../agents/_components/agent_virtual_keys.tsx | 12 +- .../_components/dynamic_agent_form_fields.tsx | 39 +- .../agents/_components/kill_switch_config.ts | 16 +- .../memory/_components/MemoryDetailDrawer.tsx | 24 +- .../memory/_components/MemoryEditModal.tsx | 40 +- .../memory/_components/MemoryTable.tsx | 17 +- .../memory/_components/MemoryTableColumns.tsx | 39 +- .../memory/_components/MemoryView.tsx | 49 +- .../src/app/(dashboard)/memory/page.tsx | 4 +- .../prompts/_components/PromptTable.tsx | 13 +- .../_components/PromptTableColumns.tsx | 45 +- .../prompts/_components/add_prompt_form.tsx | 43 +- .../(dashboard)/prompts/_components/index.tsx | 45 +- .../DeveloperMessageCard.tsx | 8 +- .../prompt_editor_view/DotpromptViewTab.tsx | 6 +- .../prompt_editor_view/ModelConfigCard.tsx | 10 +- .../prompt_editor_view/PromptCodeSnippets.tsx | 20 +- .../prompt_editor_view/PromptEditorHeader.tsx | 26 +- .../prompt_editor_view/PromptMessagesCard.tsx | 30 +- .../prompt_editor_view/PublishModal.tsx | 18 +- .../prompt_editor_view/ToolsCard.tsx | 12 +- .../VersionHistorySidePanel.tsx | 18 +- .../conversation_panel/EmptyState.tsx | 6 +- .../conversation_panel/MessageInput.tsx | 8 +- .../conversation_panel/MessageList.tsx | 7 +- .../conversation_panel/VariableInput.tsx | 6 +- .../conversation_panel/VariableWarning.tsx | 8 +- .../conversation_panel/index.tsx | 4 +- .../conversation_panel/useConversation.ts | 10 +- .../_components/prompt_editor_view/index.tsx | 22 +- .../_components/prompt_editor_view/utils.ts | 3 +- .../prompts/_components/prompt_info.tsx | 101 +- .../prompts/_components/tool_modal.tsx | 14 +- .../prompts/_components/variable_textarea.tsx | 12 +- .../_components/CreateSearchTools.tsx | 76 +- .../_components/SearchConnectionTest.tsx | 62 +- .../_components/SearchToolTable.tsx | 13 +- .../_components/SearchToolTableColumns.tsx | 52 +- .../_components/SearchToolTester.tsx | 50 +- .../_components/SearchToolView.tsx | 42 +- .../search-tools/_components/SearchTools.tsx | 97 +- .../_components/ClaudeCodePluginsPanel.tsx | 31 +- .../skills/_components/PluginTable.tsx | 9 +- .../skills/_components/PluginTableColumns.tsx | 53 +- .../skills/_components/add_plugin_form.tsx | 130 +- .../_components/CreateVectorStore.tsx | 90 +- .../_components/DocumentsTable.tsx | 9 +- .../_components/DocumentsTableColumns.tsx | 26 +- .../vector-stores/_components/IndexesTab.tsx | 47 +- .../_components/IndexesTable.tsx | 13 +- .../_components/IndexesTableColumns.tsx | 31 +- .../_components/S3VectorsConfig.tsx | 68 +- .../_components/TestVectorStoreTab.tsx | 14 +- .../_components/VectorStoreForm.tsx | 229 +-- .../_components/VectorStoreTable.tsx | 16 +- .../_components/VectorStoreTableColumns.tsx | 79 +- .../_components/VectorStoreTester.tsx | 48 +- .../vector-stores/_components/index.tsx | 49 +- .../_components/vector_store_info.tsx | 114 +- .../MakeSkillPublicForm.tsx | 63 +- .../components/claude_code_plugins/helpers.ts | 17 +- .../claude_code_plugins/skill_detail.tsx | 136 +- .../common_components/ModelSelector.tsx | 11 +- .../src/components/vector_store_providers.tsx | 169 +- ui/litellm-dashboard/src/i18n/locales/en.json | 1803 +++++++++++++++++ 79 files changed, 3942 insertions(+), 1526 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx index 3d863036234..70c7d2fdb92 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx @@ -27,6 +27,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import i18n from "@/i18n/i18n"; import type { KeyValueFormValue, KillSwitchConfig, KillSwitchFormValue } from "./kill_switch_config"; export interface AgentSkillFormValue { @@ -282,7 +283,7 @@ export const AgentTagsInput = ({ value, onValueChange, placeholder, - emptyText = "No matching options", + emptyText = i18n.t("agents.formKit.noMatchingOptions"), ...props }: AgentTagsInputProps) => { const anchor = useComboboxAnchor(); @@ -396,7 +397,7 @@ export const AgentMultiSelect = ({ value, onValueChange, placeholder, - emptyText = "No matching options", + emptyText = i18n.t("agents.formKit.noMatchingOptions"), ...props }: AgentMultiSelectProps) => { const anchor = useComboboxAnchor(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx index 12465c6e861..e891195d137 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx @@ -1,14 +1,17 @@ import React from "react"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import type { components } from "@/lib/http/schema"; import { useQuery } from "@tanstack/react-query"; import { apiClient } from "@/components/networking"; import { Button } from "@/components/ui/button"; import { readAgentIdentity } from "./agent_identity"; -const authenticationMessage = (error: boolean, lastAuthenticated?: string | null): string => { - if (error) return "Could not load authentication evidence"; - if (lastAuthenticated) return `Last authenticated identity match: ${new Date(lastAuthenticated).toLocaleString()}`; - return "Configured, awaiting an authenticated request"; +const authenticationMessage = (t: TFunction, error: boolean, lastAuthenticated?: string | null): string => { + if (error) return t("agents.identityDetails.evidence.loadFailed"); + if (lastAuthenticated) + return t("agents.identityDetails.evidence.lastMatch", { date: new Date(lastAuthenticated).toLocaleString() }); + return t("agents.identityDetails.evidence.awaiting"); }; export const AgentIdentityDetails = ({ @@ -22,6 +25,7 @@ export const AgentIdentityDetails = ({ accessToken: string | null; isAdmin: boolean; }) => { + const { t } = useTranslation(); const identity = readAgentIdentity(value); const { data, isError, isFetching, refetch } = useQuery({ queryKey: ["agent-identity", agentId, identity], @@ -36,31 +40,39 @@ export const AgentIdentityDetails = ({ }); if (!identity || !isAdmin) return null; - const executionLabel = data?.enabled ? "Enabled" : "Disabled"; + const executionLabel = data?.enabled + ? t("agents.identityDetails.execution.enabled") + : t("agents.identityDetails.execution.disabled"); return ( -
-

Agent Identity: Microsoft Entra ID

+
+

{t("agents.identityDetails.title")}

- Tenant: {identity.tenant_id} + {t("agents.identityDetails.tenant")} {identity.tenant_id}

<>

- Application (Client) ID: {identity.client_id} + {t("agents.identityDetails.clientId")} {identity.client_id} +

+

+ {t("agents.identityDetails.servicePrincipal")}{" "} + {identity.service_principal_id || t("agents.identityDetails.notConfigured")}

-

Enterprise application Object ID: {identity.service_principal_id || "Not configured"}

- Execution: {data ? executionLabel : "Loading"} · Mode: {data?.execution_mode ?? "Loading"} + {t("agents.identityDetails.executionSummary", { + execution: data ? executionLabel : t("agents.identityDetails.loading"), + mode: data?.execution_mode ?? t("agents.identityDetails.loading"), + })}

{data?.identity?.active === false - ? "Identity unbound; execution is disabled" - : authenticationMessage(isError, data?.last_authenticated_at)} -

-

- Recent evidence comes from a validated Entra token matching this binding. It is persisted across restarts and - cleared when the binding changes. Tool and model permissions are checked separately. + ? t("agents.identityDetails.evidence.unbound") + : authenticationMessage(t, isError, data?.last_authenticated_at)}

+

{t("agents.identityDetails.evidenceNote")}

- View request logs + {t("agents.identityDetails.viewLogs")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx index 50c60776ff3..d4af3ff3833 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx @@ -1,26 +1,30 @@ import React, { useEffect, useState } from "react"; import { useWatch } from "react-hook-form"; +import type { ParseKeys } from "i18next"; +import { Trans, useTranslation } from "react-i18next"; +import i18n from "@/i18n/i18n"; import { apiClient } from "@/components/networking"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { AgentFormField, type AgentFormValues } from "./AgentFormKit"; import { entraTenantFromIssuer, IDENTITY_UUID_PATTERN } from "./agent_identity"; -const PROVIDER_OPTIONS = [ - { value: "none", label: "No explicit identity binding" }, - { value: "microsoft_entra", label: "Microsoft Entra ID" }, +const PROVIDER_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "none", labelKey: "agents.identityFields.providers.none" }, + { value: "microsoft_entra", labelKey: "agents.identityFields.providers.microsoftEntra" }, ]; -const EXECUTION_MODE_OPTIONS = [ - { value: "autonomous", label: "Autonomous" }, - { value: "delegated", label: "On behalf of a user" }, - { value: "both", label: "Both" }, +const EXECUTION_MODE_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "autonomous", labelKey: "agents.identityFields.executionModes.autonomous" }, + { value: "delegated", labelKey: "agents.identityFields.executionModes.delegated" }, + { value: "both", labelKey: "agents.identityFields.executionModes.both" }, ]; -const EXECUTION_OPTIONS = [ - { value: "enabled", label: "Enabled" }, - { value: "disabled", label: "Disabled" }, +const EXECUTION_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "enabled", labelKey: "agents.identityFields.execution.enabled" }, + { value: "disabled", labelKey: "agents.identityFields.execution.disabled" }, ]; export const AgentIdentityFields = ({ accessToken }: { accessToken: string | null }) => { + const { t } = useTranslation(); const provider = useWatch({ name: "identity_provider" }); const mode = useWatch({ name: "execution_mode" }); const showScopes = mode !== "autonomous" && mode !== undefined; @@ -44,7 +48,7 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul } }) .catch(() => { - if (active) setError("Could not load the gateway's trusted identity providers"); + if (active) setError(i18n.t("agents.identityFields.errors.providersLoadFailed")); }); return () => { active = false; @@ -53,18 +57,18 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul return ( <> -
+
-

Agent Identity

-

- Connect an existing identity provider application to this agent. Its name and runtime address can change - independently. -

+

{t("agents.identityFields.title")}

+

{t("agents.identityFields.description")}

- + {({ value, onChange, id }) => ( - + {tenants.map((tenant) => ( @@ -109,26 +113,24 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul

)} {!error && tenants.length === 0 && ( -

- No trusted Entra tenant is available. Configure JWT issuer and audience validation on the gateway first. - Dashboard Microsoft SSO is configured separately. -

+

{t("agents.identityFields.tenant.noneAvailable")}

)} - Find this under{" "} - - Entra App registrations - - , select your agent application, then Overview. No client secret is required here. - + + ), + }} + /> } > {({ value, onChange, ref, ...control }) => ( @@ -141,10 +143,14 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul /> )} - + {({ value, onChange, id }) => ( {({ value, onChange, ref, ...control }) => ( )} -

- Users must first sign in through this gateway's Microsoft SSO. Subsequent delegated calls must - satisfy both user and agent permissions. -

+

{t("agents.identityFields.requiredScopes.note")}

)} - + {({ value, onChange, id }) => ( )} -

- LiteLLM verifies the agent's Entra token before matching this identity. Saving these fields - configures the binding; an authenticated request provides verification. Runtime authentication headers are - configured separately. -

+

{t("agents.identityFields.verificationNote")}

)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx index 8d68a8f921b..e733a564576 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx @@ -1,5 +1,6 @@ import { CircleAlert } from "lucide-react"; import React, { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -24,6 +25,7 @@ const AgentKillSwitchDangerZone: React.FC = ({ accessToken, isAdmin, }) => { + const { t } = useTranslation(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [confirmationInput, setConfirmationInput] = useState(""); const [isFiring, setIsFiring] = useState(false); @@ -44,9 +46,9 @@ const AgentKillSwitchDangerZone: React.FC = ({ const result = await triggerAgentKillSwitchCall(accessToken, agentId); setLastResult(result); setIsConfirmOpen(false); - toast.success(`Kill switch fired (HTTP ${result.status_code})`); + toast.success(t("agents.dangerZone.toasts.fired", { status: result.status_code })); } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to fire kill switch"); + toast.error(error instanceof Error ? error.message : t("agents.dangerZone.toasts.fireFailed")); } finally { setIsFiring(false); } @@ -55,38 +57,33 @@ const AgentKillSwitchDangerZone: React.FC = ({ return (

- Danger Zone + {t("agents.dangerZone.title")}

-

Kill switch

+

{t("agents.dangerZone.killSwitch")}

{killSwitch ? ( <> -

- Calls the configured webhook to stop this agent's upstream runtime. This can cause an outage for - everyone using the agent and cannot be undone from LiteLLM -

+

{t("agents.dangerZone.configuredDescription")}

{killSwitch.method ?? "POST"} {killSwitch.url}

) : ( -

- Not configured. Add a kill switch webhook under Settings to enable this action -

+

{t("agents.dangerZone.notConfigured")}

)}
{killSwitch && ( )}
{lastResult && (

- Last result: HTTP {lastResult.status_code} + {t("agents.dangerZone.lastResult", { status: lastResult.status_code })} {lastResult.response_body ? ` ${lastResult.response_body}` : ""}

)} @@ -95,27 +92,32 @@ const AgentKillSwitchDangerZone: React.FC = ({ !open && !isFiring && setIsConfirmOpen(false)}> - Fire kill switch for {agentName}? + {t("agents.dangerZone.confirm.title", { name: agentName })}
- This can cause an outage + {t("agents.dangerZone.confirm.outageTitle")} - LiteLLM will call {killSwitch?.method ?? "POST"} {killSwitch?.url} immediately. Whatever that webhook - does to the agent is outside LiteLLM's control and cannot be reverted here + {t("agents.dangerZone.confirm.outageDescription", { + method: killSwitch?.method ?? "POST", + url: killSwitch?.url, + })}

- Type {agentName} to confirm: + {agentName} }} + />

setConfirmationInput(e.target.value)} placeholder={agentName} @@ -126,7 +128,7 @@ const AgentKillSwitchDangerZone: React.FC = ({
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx index 3f599c38976..308cefec844 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx @@ -1,6 +1,7 @@ import { useTerminology } from "@/contexts/TerminologyContext"; import React, { useState, useEffect } from "react"; import { Info, Plus } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { getAgentsList, deleteAgentCall } from "@/components/networking"; import AddAgentForm from "./add_agent_form"; import { isAdminRole } from "@/utils/roles"; @@ -33,6 +34,7 @@ interface AgentsResponse { const AgentsPanel: React.FC = ({ accessToken, userRole, teams }) => { const term = useTerminology(); + const { t } = useTranslation(); const [agentsList, setAgentsList] = useState([]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -122,11 +124,11 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams setIsDeleting(true); try { await deleteAgentCall(accessToken, agentToDelete.id); - toast.success(`Agent "${agentToDelete.name}" deleted successfully`); + toast.success(t("agents.panel.toasts.deleted", { name: agentToDelete.name })); await refetchAgents(healthCheckEnabled); } catch (error) { console.error("Error deleting agent:", error); - toast.fromError("Failed to delete agent"); + toast.fromError(t("agents.panel.toasts.deleteFailed")); } finally { setIsDeleting(false); setAgentToDelete(null); @@ -140,25 +142,18 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams return (
-

Agents

-

- {term( - "List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents public.", - )} -

+

{t("agents.panel.title")}

+

{term(t("agents.panel.description"))}

- How do agents authenticate? - - Agents can authenticate with a virtual key or a trusted identity provider using JWT. Configure an identity - binding when adding or editing an agent. JWT authentication does not require a virtual key. - + {t("agents.panel.authAlert.title")} + {t("agents.panel.authAlert.description")} {isAdmin && (
)} @@ -201,15 +196,15 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams > - Delete Agent + {t("agents.panel.deleteDialog.title")} - Are you sure you want to delete agent: {agentToDelete.name}? This action cannot be undone. + {t("agents.panel.deleteDialog.description", { name: agentToDelete.name })} - Cancel + {t("agents.panel.deleteDialog.cancel")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx index d737a9250eb..421207e72d0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx @@ -3,6 +3,7 @@ import { SortingState } from "@tanstack/react-table"; import { Bot, CircleCheck, Search as SearchIcon, X } from "lucide-react"; import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Agent } from "@/components/agents/types"; import { DataTable } from "@/components/shared/DataTable"; @@ -27,16 +28,17 @@ interface AgentsTableProps { const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }]; function EmptyState({ isFiltered }: { isFiltered: boolean }) { + const { t } = useTranslation(); return (
-
{isFiltered ? "No matching agents" : "No agents yet"}
+
+ {isFiltered ? t("agents.table.empty.filteredTitle") : t("agents.table.empty.title")} +
- {isFiltered - ? "Adjust the search to see more agents." - : "Add an agent to make it available in your organization."} + {isFiltered ? t("agents.table.empty.filteredDescription") : t("agents.table.empty.description")}
); @@ -52,6 +54,7 @@ const AgentsTable: React.FC = ({ onAgentClick, onDeleteClick, }) => { + const { t } = useTranslation(); const [sorting, setSorting] = useState(DEFAULT_SORTING); const [searchTerm, setSearchTerm] = useState(""); const filteredAgents = useMemo( @@ -79,7 +82,7 @@ const AgentsTable: React.FC = ({ sorting={sorting} onSortingChange={setSorting} isLoading={isLoading} - loadingMessage="Loading agents…" + loadingMessage={t("agents.table.loading")} noDataMessage={ 0} />} size="compact" toolbar={() => ( @@ -89,13 +92,17 @@ const AgentsTable: React.FC = ({ setSearchTerm(e.target.value)} /> {searchTerm && ( - setSearchTerm("")}> + setSearchTerm("")} + > @@ -109,7 +116,7 @@ const AgentsTable: React.FC = ({ - Health Check + {t("agents.table.healthCheck.label")} = ({
} /> - When enabled, only agents with reachable URLs are shown + {t("agents.table.healthCheck.hint")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx index 002219f5478..a0e8751e682 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import { useTranslation } from "react-i18next"; import { MoreHorizontal, Trash2 } from "lucide-react"; import { Agent } from "@/components/agents/types"; @@ -14,6 +15,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import i18n from "@/i18n/i18n"; import { cn } from "@/lib/cva.config"; interface AgentRowActionsProps { @@ -22,10 +24,11 @@ interface AgentRowActionsProps { } function AgentRowActions({ agent, onDeleteClick }: AgentRowActionsProps) { + const { t } = useTranslation(); return ( @@ -38,7 +41,7 @@ function AgentRowActions({ agent, onDeleteClick }: AgentRowActionsProps) { onClick={() => onDeleteClick(agent.agent_id, agent.agent_name)} > - Delete + {t("agents.table.actions.delete")} @@ -59,8 +62,8 @@ export const getAgentsTableColumns = ({ { id: "agent_name", accessorKey: "agent_name", - meta: { title: "Agent Name" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.name") }, + header: ({ column }) => , size: 200, enableSorting: true, cell: ({ row }) => { @@ -75,8 +78,8 @@ export const getAgentsTableColumns = ({ { id: "agent_id", accessorKey: "agent_id", - meta: { title: "Agent ID" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.id") }, + header: ({ column }) => , size: 200, enableSorting: true, cell: ({ row }) => ( @@ -90,22 +93,22 @@ export const getAgentsTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.spend"), numeric: true }, + header: ({ column }) => , size: 130, enableSorting: true, cell: ({ row }) => , }, { id: "model", - meta: { title: "Model" }, - header: "Model", + meta: { title: i18n.t("agents.table.columns.model") }, + header: i18n.t("agents.table.columns.model"), size: 170, enableSorting: false, cell: ({ row }) => { const model = row.original.litellm_params?.model; if (!model) { - return N/A; + return {i18n.t("agents.table.noModel")}; } return ( @@ -122,25 +125,26 @@ export const getAgentsTableColumns = ({ const timestamp = agent.created_at ? new Date(agent.created_at).getTime() : 0; return Number.isNaN(timestamp) ? 0 : timestamp; }, - meta: { title: "Created" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.created") }, + header: ({ column }) => , size: 150, enableSorting: true, cell: ({ row }) => , }, { id: "status", - meta: { title: "Status" }, - header: "Status", + meta: { title: i18n.t("agents.table.columns.status") }, + header: i18n.t("agents.table.columns.status"), size: 130, enableSorting: false, cell: ({ row }) => { const hasKeys = (row.original.keys?.length ?? 0) > 0; - if (row.original.jwt_auth_configured) return ; + if (row.original.jwt_auth_configured) + return ; return hasKeys ? ( - + ) : ( - + ); }, }, @@ -149,7 +153,7 @@ export const getAgentsTableColumns = ({ { id: "actions", meta: { className: "text-right", headerClassName: "text-right" }, - header: () => Actions, + header: () => {i18n.t("agents.table.columns.actions")}, size: 64, enableSorting: false, enableHiding: false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx index 578d80da6ed..2b9a0edb70d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -7,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Textarea } from "@/components/ui/textarea"; import { Field, FieldTitle } from "@/components/ui/field"; import { PasswordInput } from "@/components/shared/PasswordInput"; +import i18n from "@/i18n/i18n"; import { AgentFormField, AgentFormValues, labelWithHint } from "./AgentFormKit"; import { KILL_SWITCH_AUTH_TYPES, KILL_SWITCH_METHODS, validateKillSwitchBody } from "./kill_switch_config"; @@ -21,6 +23,7 @@ const KeyValueFieldArray = ({ keyPlaceholder: string; valuePlaceholder: string; }) => { + const { t } = useTranslation(); const { control } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, name }); @@ -28,7 +31,10 @@ const KeyValueFieldArray = ({
{fields.map((item, index) => (
- + {({ value, onChange, ref, ...control }) => ( remove(index)} > @@ -109,24 +115,50 @@ const TextField = ({ ); const KillSwitchAuthFields = () => { + const { t } = useTranslation(); const { control } = useFormContext(); const authType = useWatch({ control, name: "kill_switch.auth_type" }); switch (authType) { case "bearer": - return ; + return ( + + ); case "api_key": return ( <> - - + + ); case "basic": return ( <> - - + + ); default: @@ -139,13 +171,13 @@ const KillSwitchFormFields = () => ( - + {({ value, onChange, ref: _ref, ...control }) => ( @@ -208,7 +240,7 @@ const KillSwitchFormFields = () => ( {KILL_SWITCH_AUTH_TYPES.map((option) => ( - {option.label} + {i18n.t(option.labelKey)} ))} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx index 188d3349beb..715f2516e66 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx @@ -5,6 +5,7 @@ import { FormProvider, useForm, useWatch } from "react-hook-form"; import { toast } from "@/lib/toast"; import { Logo } from "@/components/molecules/logo/Logo"; import { Bot, Check, CircleCheck, Key, LayoutGrid } from "lucide-react"; +import { Trans, useTranslation } from "react-i18next"; import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay"; import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/shared/table_cells/status_badge"; @@ -55,10 +56,17 @@ import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermis import AccessGroupSelector from "@/components/common_components/AccessGroupSelector"; import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import i18n from "@/i18n/i18n"; const CUSTOM_AGENT_TYPE = "custom"; -const STEP_TITLES = ["Configure", "Entitlements", "Governance", "Agent Management", "Ready"] as const; +const STEP_TITLES = [ + "agents.addForm.steps.configure", + "agents.addForm.steps.entitlements", + "agents.addForm.steps.governance", + "agents.addForm.steps.agentManagement", + "agents.addForm.steps.ready", +] as const; const stepMarkerClass = (index: number, current: number): string => { if (index < current) return "border-primary text-primary"; @@ -73,11 +81,12 @@ const stepTitleClass = (index: number, current: number): string => { }; const AgentTypeLabel: React.FC<{ agentType: string; info: AgentCreateInfo | undefined }> = ({ agentType, info }) => { + const { t } = useTranslation(); if (agentType === CUSTOM_AGENT_TYPE) { return ( - Custom / Other + {t("agents.addForm.agentType.custom")} ); } @@ -91,7 +100,7 @@ const AgentTypeLabel: React.FC<{ agentType: string; info: AgentCreateInfo | unde }; const StepProgress: React.FC<{ current: number }> = ({ current }) => ( -
    +
      {STEP_TITLES.map((title, index) => (
    1. = ({ current }) => ( > {index < current ? : index + 1} - {title} + {i18n.t(title)} {index < STEP_TITLES.length - 1 &&
    2. ))} @@ -132,6 +141,7 @@ interface AddAgentFormProps { } const AddAgentForm: React.FC = ({ visible, onClose, accessToken, onSuccess, teams }) => { + const { t } = useTranslation(); const { userId, userRole } = useAuthorized(); const form = useForm({ defaultValues: buildInitialValues("a2a") }); const panels = useCollapsiblePanels([AGENT_FORM_CONFIG.basic.key]); @@ -339,12 +349,12 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const handleCreateAgent = async () => { if (!accessToken) { - toast.error("No access token available"); + toast.error(t("agents.addForm.toasts.noAccessToken")); return; } if (keyAssignOption === "existing_key" && !selectedExistingKey) { - toast.error("Please select an existing key to assign"); + toast.error(t("agents.addForm.toasts.selectExistingKey")); return; } @@ -358,7 +368,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const values = form.getValues(); const built = buildAgentData(values); if (!built) { - toast.error("Failed to build agent data"); + toast.error(t("agents.addForm.toasts.buildFailed")); setIsSubmitting(false); return; } @@ -434,7 +444,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok } catch (error) { console.error("Error creating agent:", error); const errorMessage = error instanceof Error ? error.message : String(error); - toast.error(errorMessage ? `Failed to create agent: ${errorMessage}` : "Failed to create agent"); + toast.error( + errorMessage + ? t("agents.addForm.toasts.createFailedWithError", { error: errorMessage }) + : t("agents.addForm.toasts.createFailed"), + ); } finally { setIsSubmitting(false); } @@ -461,17 +475,14 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const renderEntitlementsStep = () => (
      -

      - Configure which models, agents, and MCP tools this agent is allowed to use. Leave fields empty to allow all - (subject to key/team permissions). -

      +

      {t("agents.addForm.entitlements.description")}

      {({ id, value, onChange }) => ( @@ -479,7 +490,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok id={id} value={Array.isArray(value) ? (value as string[]) : []} onValueChange={onChange} - placeholder={loadingModels ? "Loading models..." : "Select models (leave empty for all)"} + placeholder={ + loadingModels + ? t("agents.addForm.entitlements.models.loading") + : t("agents.addForm.entitlements.models.placeholder") + } options={availableModels.map((m) => ({ label: getModelDisplayName(m), value: m }))} /> )} @@ -488,8 +503,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ id, value, onChange }) => ( @@ -497,7 +512,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok id={id} value={Array.isArray(value) ? (value as string[]) : []} onValueChange={onChange} - placeholder={loadingAgents ? "Loading agents..." : "Select agents (leave empty for all)"} + placeholder={ + loadingAgents + ? t("agents.addForm.entitlements.agents.loading") + : t("agents.addForm.entitlements.agents.placeholder") + } options={availableAgents.map((a) => ({ label: a.agent_name, value: a.agent_id }))} /> )} @@ -506,15 +525,15 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ value, onChange }) => ( )} @@ -524,8 +543,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ value, onChange }) => ( @@ -536,7 +555,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok accessGroups: (value as McpServerSelection | undefined)?.accessGroups ?? [], }} accessToken={accessToken ?? ""} - placeholder="Select MCP servers or access groups (optional)" + placeholder={t("agents.addForm.entitlements.mcpServers.placeholder")} /> )} @@ -574,15 +593,15 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const renderObservabilityStep = () => (
      -

      Tracing

      +

      {t("agents.addForm.governance.tracing.title")}

      - Require x-litellm-trace-id on calls TO this agent + {t("agents.addForm.governance.tracing.inbound.label", { header: "x-litellm-trace-id" })}

      - Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent). + {t("agents.addForm.governance.tracing.inbound.description")}

      @@ -591,10 +610,10 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
      - Require x-litellm-trace-id on calls BY this agent + {t("agents.addForm.governance.tracing.outbound.label", { header: "x-litellm-trace-id" })}

      - Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking. + {t("agents.addForm.governance.tracing.outbound.description", { header: "x-litellm-trace-id" })}

      = ({ visible, onClose, accessTok
      -

      Budgets & Rate Limits

      +

      {t("agents.addForm.governance.budgets.title")}

      {!requireTraceIdOutbound && (
      - Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and - rate limits. + {t("agents.addForm.governance.budgets.requiresOutbound", { header: "x-litellm-trace-id" })}
      )} -
      Session Budgets
      +
      + {t("agents.addForm.governance.budgets.session.title")} +
      - Max Iterations + + {t("agents.addForm.governance.budgets.session.maxIterations.label")} + @@ -639,15 +661,19 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok } onBlur={() => setMaxIterations((current) => (current !== null && current < 1 ? 1 : current))} /> -

      Hard cap on LLM calls per session

      +

      + {t("agents.addForm.governance.budgets.session.maxIterations.hint")} +

      - Max Budget Per Session ($) + + {t("agents.addForm.governance.budgets.session.maxBudget.label")} + @@ -657,26 +683,48 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok setMaxBudgetPerSession((current) => (current !== null && current < 0.01 ? 0.01 : current)) } /> -

      Max spend per trace before returning 429

      +

      + {t("agents.addForm.governance.budgets.session.maxBudget.hint")} +

      -
      Agent Rate Limits
      -

      Global rate limits applied across all callers of this agent.

      +
      + {t("agents.addForm.governance.rateLimits.agent.title")} +
      +

      {t("agents.addForm.governance.rateLimits.agent.description")}

      - {rateLimitField("tpm_limit", "TPM Limit", "e.g. 100000")} - {rateLimitField("rpm_limit", "RPM Limit", "e.g. 100")} + {rateLimitField( + "tpm_limit", + t("agents.addForm.governance.rateLimits.agent.tpm.label"), + t("agents.addForm.governance.rateLimits.agent.tpm.placeholder"), + )} + {rateLimitField( + "rpm_limit", + t("agents.addForm.governance.rateLimits.agent.rpm.label"), + t("agents.addForm.governance.rateLimits.agent.rpm.placeholder"), + )}
      -
      Per-Session Rate Limits
      +
      + {t("agents.addForm.governance.rateLimits.session.title")} +

      - Rate limits per session (x-litellm-trace-id). Each session gets its own counters. + {t("agents.addForm.governance.rateLimits.session.description", { header: "x-litellm-trace-id" })}

      - {rateLimitField("session_tpm_limit", "Session TPM Limit", "e.g. 10000")} - {rateLimitField("session_rpm_limit", "Session RPM Limit", "e.g. 20")} + {rateLimitField( + "session_tpm_limit", + t("agents.addForm.governance.rateLimits.session.tpm.label"), + t("agents.addForm.governance.rateLimits.session.tpm.placeholder"), + )} + {rateLimitField( + "session_rpm_limit", + t("agents.addForm.governance.rateLimits.session.rpm.label"), + t("agents.addForm.governance.rateLimits.session.rpm.placeholder"), + )}
      @@ -684,10 +732,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
      -

      Guardrails

      -

      - Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent. -

      +

      {t("agents.addForm.governance.guardrails.title")}

      +

      {t("agents.addForm.governance.guardrails.description")}

      {({ value, onChange }) => ( = ({ visible, onClose, accessTok <> - {labelWithHint("Agent Type", "Select the type of agent you want to create")} + {labelWithHint(t("agents.addForm.configure.agentType.label"), t("agents.addForm.configure.agentType.hint"))} )} - + {({ value, onChange, ref, ...control }) => (