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")}
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 }) => (
({ value: option.value, label: t(option.labelKey) }))}
value={typeof value === "string" ? value : "none"}
onValueChange={onChange}
>
@@ -74,7 +78,7 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
{PROVIDER_OPTIONS.map((option) => (
- {option.label}
+ {t(option.labelKey)}
))}
@@ -85,13 +89,13 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
<>
{({ 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: option.value, label: t(option.labelKey) }))}
value={typeof value === "string" ? value : "autonomous"}
onValueChange={onChange}
>
@@ -154,7 +160,7 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
{EXECUTION_MODE_OPTIONS.map((option) => (
- {option.label}
+ {t(option.labelKey)}
))}
@@ -163,25 +169,25 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
- Open{" "}
-
- Entra Enterprise applications
-
- , select this application, and copy its Object ID. The App registrations Object ID is a different
- value.
- >
+
+ ),
+ }}
+ />
}
>
{({ value, onChange, ref, ...control }) => (
@@ -191,8 +197,8 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
{({ value, onChange, ref, ...control }) => (
{({ 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 }) => (
({ value: option.value, label: t(option.labelKey) }))}
value={value === false ? "disabled" : "enabled"}
onValueChange={(next) => onChange(next === "enabled")}
>
@@ -241,18 +244,14 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
{EXECUTION_OPTIONS.map((option) => (
- {option.label}
+ {t(option.labelKey)}
))}
)}
-
- 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 && (
{isFiring && }
- Fire Kill Switch
+ {t("agents.dangerZone.fire")}
)}
{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 = ({
setIsConfirmOpen(false)} disabled={isFiring}>
- Cancel
+ {t("agents.dangerZone.confirm.cancel")}
= ({
aria-busy={isFiring}
>
{isFiring && }
- {isFiring ? "Firing..." : "Fire Kill Switch"}
+ {isFiring ? t("agents.dangerZone.confirm.firing") : t("agents.dangerZone.fire")}
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 && (
- Add New Agent
+ {t("agents.panel.addAgent")}
)}
@@ -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")}
- Delete
+ {t("agents.panel.deleteDialog.confirm")}
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 }) => (
@@ -163,28 +195,28 @@ const KillSwitchFormFields = () => (
- Headers
+ {i18n.t("agents.killSwitchFields.headers.title")}
- Query Parameters
+ {i18n.t("agents.killSwitchFields.queryParams.title")}
validateKillSwitchBody(typeof value === "string" ? value : "") }}
>
{({ value, onChange, ref, ...control }) => (
@@ -199,7 +231,7 @@ 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) => (
= ({ current }) => (
>
{index < current ? : index + 1}
- {title}
+ {i18n.t(title)}
{index < STEP_TITLES.length - 1 && }
))}
@@ -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")}
+
-
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 !== null && handleAgentTypeChange(value)}>
@@ -785,18 +831,22 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
))}
- Not listed?
+ {t("agents.addForm.configure.agentType.notListed")}
- Custom / Other
-
+ {t("agents.addForm.agentType.custom")}
+
- For outbound agents using an identity provider or virtual key
+ {t("agents.addForm.configure.agentType.customDescription")}
@@ -810,24 +860,28 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
{agentType === CUSTOM_AGENT_TYPE ? (
-
+
{({ value, onChange, ref, ...control }) => (
)}
-
+
{({ value, onChange, ref, ...control }) => (
@@ -842,7 +896,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
{selectedAgentTypeInfo.credential_fields.length > 0 && (
- {selectedAgentTypeInfo.agent_type_display_name} Settings
+ {t("agents.addForm.configure.typeSettings", { type: selectedAgentTypeInfo.agent_type_display_name })}
{selectedAgentTypeInfo.credential_fields.map((field) => (
@@ -851,7 +905,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
name={field.key}
label={field.tooltip ? labelWithHint(field.label, field.tooltip) : field.label}
defaultValue={field.default_value ?? undefined}
- rules={field.required ? { required: `Please enter ${field.label}` } : undefined}
+ rules={
+ field.required
+ ? { required: t("agents.addForm.configure.enterField", { field: field.label }) }
+ : undefined
+ }
>
{({ value, onChange, ref, ...control }) =>
field.field_type === "password" ? (
@@ -901,7 +959,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
);
const renderAssignKeyStep = () => {
- const agentName = form.getValues("agent_name") || "your-agent";
+ const agentName = form.getValues("agent_name") || t("agents.addForm.assignKey.agentNameFallback");
return (
{/* Agent name chip */}
@@ -914,10 +972,7 @@ const AddAgentForm: React.FC
= ({ visible, onClose, accessTok
{({ value, onChange }) => (
@@ -927,9 +982,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
{form.getValues("identity_provider") === "microsoft_entra" && (
-
- This agent will authenticate with Microsoft Entra ID. You can skip virtual key creation.
-
+ {t("agents.addForm.assignKey.entraNotice")}
)}
= ({ visible, onClose, accessTok
>
-
+
- Create a new key for this agent
+ {t("agents.addForm.assignKey.createNew.title")}
-
A dedicated key scoped to this agent.
+
+ {t("agents.addForm.assignKey.createNew.description")}
+
{keyAssignOption === "create_new" && (
e.stopPropagation()}>
- Key Name
+
+ {t("agents.addForm.assignKey.createNew.keyName.label")}
+
setNewKeyName(e.target.value)}
- placeholder="e.g. my-agent-key"
+ placeholder={t("agents.addForm.assignKey.createNew.keyName.placeholder")}
/>
)}
-
+
@@ -983,18 +1040,24 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
onClick={() => setKeyAssignOption("existing_key")}
>
-
+
- Assign an existing key
+ {t("agents.addForm.assignKey.existing.title")}
-
Re-assign a key you already have to this agent.
+
+ {t("agents.addForm.assignKey.existing.description")}
+
{keyAssignOption === "existing_key" && (
e.stopPropagation()}>
({
@@ -1016,8 +1079,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
onClick={() => setKeyAssignOption("skip")}
>
{form.getValues("identity_provider") === "microsoft_entra"
- ? "Use Entra JWT authentication"
- : "Skip for now, I’ll assign a key later"}
+ ? t("agents.addForm.assignKey.skip.entra")
+ : t("agents.addForm.assignKey.skip.later")}
@@ -1027,7 +1090,7 @@ const AddAgentForm: React.FC
= ({ visible, onClose, accessTok
const renderReadyStep = () => (
-
Agent Created!
+
{t("agents.addForm.ready.title")}
@@ -1041,14 +1104,17 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
)}
{assignedKeyAlias && (
- Key {assignedKeyAlias} has been assigned to this agent.
+ {assignedKeyAlias} }}
+ />
)}
{!createdKeyValue && !assignedKeyAlias && keyAssignOption === "skip" && (
{form.getValues("identity_provider") === "microsoft_entra"
- ? "Microsoft Entra ID is configured. Send an authenticated agent request to verify the connection."
- : "No key assigned. You can create one from the Virtual Keys page."}
+ ? t("agents.addForm.ready.entraConfigured")
+ : t("agents.addForm.ready.noKey")}
)}
@@ -1060,9 +1126,15 @@ const AddAgentForm: React.FC
= ({ visible, onClose, accessTok
{selectedLogo && currentStep < 1 && (
-
+
)}
- Add New Agent
+
+ {t("agents.addForm.dialog.title")}
+
@@ -1083,24 +1155,24 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
{currentStep > 0 && currentStep < 4 && (
- ← Back
+ {t("agents.addForm.dialog.back")}
)}
{currentStep < 4 && (
- Cancel
+ {t("agents.addForm.dialog.cancel")}
)}
- {currentStep < 3 && Next → }
+ {currentStep < 3 && {t("agents.addForm.dialog.next")} }
{currentStep === 3 && (
{isSubmitting && }
- {isSubmitting ? "Creating..." : "Create Agent →"}
+ {isSubmitting ? t("agents.addForm.dialog.creating") : t("agents.addForm.dialog.create")}
)}
- {currentStep === 4 && Done }
+ {currentStep === 4 && {t("agents.addForm.dialog.done")} }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_card_discovery.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_card_discovery.tsx
index aba16a50508..35df5d2ff3d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_card_discovery.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_card_discovery.tsx
@@ -2,6 +2,8 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ChevronDown, CircleAlert, CircleCheck, Info, Link as LinkIcon, RotateCw, Search, X } from "lucide-react";
+import { Trans, useTranslation } from "react-i18next";
+import i18n from "@/i18n/i18n";
import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer";
import { DiscoveredAgentCard, discoverAgentCardCall } from "@/components/networking";
@@ -63,6 +65,7 @@ const AgentCardDiscovery: React.FC = ({
discoveryRequest,
savedAgentCard,
}) => {
+ const { t } = useTranslation();
// When the parent drives discovery, ``manualUrl`` is unused — the URL
// comes from ``discoveryRequest.url`` directly. When the parent hasn't
// supplied a plan, the admin types into this field manually.
@@ -117,14 +120,16 @@ const AgentCardDiscovery: React.FC = ({
const handleDiscover = useCallback(async () => {
if (!accessToken) {
- setError("No access token available");
+ setError(i18n.t("agents.cardDiscovery.errors.noAccessToken"));
onApplyRef.current(null);
return;
}
const trimmed = effectiveUrl.trim();
if (!trimmed) {
setError(
- isParentDriven ? "Fill in the agent's connection details above first" : "Enter the agent's base URL first",
+ isParentDriven
+ ? i18n.t("agents.cardDiscovery.errors.fillConnectionDetails")
+ : i18n.t("agents.cardDiscovery.errors.enterBaseUrl"),
);
setCard(null);
onApplyRef.current(null);
@@ -152,7 +157,7 @@ const AgentCardDiscovery: React.FC = ({
resetSelections(response.agent_card);
} catch (e: any) {
if (requestId !== discoverRequestIdRef.current) return;
- setError(e?.message ? String(e.message) : "Failed to discover agent card");
+ setError(e?.message ? String(e.message) : i18n.t("agents.cardDiscovery.errors.discoverFailed"));
setCard(null);
lastSyncedSelectionRef.current = null;
onApplyRef.current(null);
@@ -247,13 +252,13 @@ const AgentCardDiscovery: React.FC = ({
if (card) return ;
return ;
};
- const discoverLabel = card ? "Re-discover" : "Discover";
+ const discoverLabel = card ? t("agents.cardDiscovery.rediscover") : t("agents.cardDiscovery.discover");
return (
- Discover from agent URL
+ {t("agents.cardDiscovery.title")}
= ({
}
/>
- LiteLLM will fetch /.well-known/agent-card.json from this URL and let you pick which skills and
- capabilities to expose through the proxy.
+ {t("agents.cardDiscovery.titleHint", { path: "/.well-known/agent-card.json" })}
{isParentDriven ? (
<>
-
- Using the connection details you entered above. We'll fetch:
-
+
{t("agents.cardDiscovery.parentDrivenIntro")}
{discoveryRequest!.display_url || effectiveUrl || (
- Fill in the fields above first
+ {t("agents.cardDiscovery.fillFieldsFirst")}
)}
@@ -290,8 +292,14 @@ const AgentCardDiscovery: React.FC
= ({
) : (
<>
- Paste the upstream agent's base URL. We'll try /.well-known/agent-card.json,{" "}
- /.well-known/agent.json, and /agent.json in order.
+ /.well-known/agent-card.json,
+ agentfile: /.well-known/agent.json,
+ rootfile: /agent.json,
+ }}
+ />
@@ -315,10 +323,15 @@ const AgentCardDiscovery: React.FC
= ({
{error && (
- Discovery failed
+ {t("agents.cardDiscovery.failedTitle")}
{error}
- setError(null)}>
+ setError(null)}
+ >
@@ -335,7 +348,7 @@ const AgentCardDiscovery: React.FC = ({
- Upstream card loaded
+ {t("agents.cardDiscovery.cardLoaded")}
{card.version && v{card.version} }
{card.provider?.organization && {card.provider.organization} }
@@ -343,18 +356,24 @@ const AgentCardDiscovery: React.FC
= ({
@@ -366,17 +385,19 @@ const AgentCardDiscovery: React.FC = ({
render={
- Skills
+ {t("agents.cardDiscovery.skills")}
}
/>
- {selectedSkillCount} / {skillCount} selected
+ {t("agents.cardDiscovery.skillsSelected", { selected: selectedSkillCount, total: skillCount })}
{skillCount === 0 ? (
- Upstream card has no skills
+
+ {t("agents.cardDiscovery.noSkills")}
+
) : (
{(card.skills ?? []).map((skill, idx) => {
@@ -394,9 +415,9 @@ const AgentCardDiscovery: React.FC
= ({
{skill.name || id}
{skill.id && {skill.id} }
- {(skill.tags ?? []).map((t: string) => (
-
- {t}
+ {(skill.tags ?? []).map((tag: string) => (
+
+ {tag}
))}
@@ -418,7 +439,9 @@ const AgentCardDiscovery: React.FC = ({
render={
- Capabilities
+
+ {t("agents.cardDiscovery.capabilities")}
+
}
/>
@@ -431,10 +454,7 @@ const AgentCardDiscovery: React.FC = ({
}
/>
-
- Only capabilities LiteLLM can faithfully proxy today are listed. Others (push notifications,
- extensions) are coming soon.
-
+ {t("agents.cardDiscovery.capabilitiesHint")}
@@ -449,7 +469,7 @@ const AgentCardDiscovery: React.FC = ({
>
{key}
- {!upstreamHas && not advertised upstream }
+ {!upstreamHas && {t("agents.cardDiscovery.notAdvertised")} }
= ({ agent }) => {
+ const { t } = useTranslation();
const params = agent.litellm_params;
if (params?.cost_per_query == null && params?.input_cost_per_token == null && params?.output_cost_per_token == null) {
@@ -14,15 +16,15 @@ const AgentCostView: React.FC = ({ agent }) => {
const rows = (
[
- ["Cost Per Query", params.cost_per_query],
- ["Input Cost Per Token", params.input_cost_per_token],
- ["Output Cost Per Token", params.output_cost_per_token],
+ [t("agents.costView.costPerQuery"), params.cost_per_query],
+ [t("agents.costView.inputCostPerToken"), params.input_cost_per_token],
+ [t("agents.costView.outputCostPerToken"), params.output_cost_per_token],
] as const
).filter(([, value]) => value != null);
return (
-
Cost Configuration
+
{t("agents.costView.title")}
{rows.map(([label, value]) => (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_form_fields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_form_fields.tsx
index bf32af30fb6..41e25d14054 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_form_fields.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_form_fields.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { useFieldArray, useFormContext } 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";
@@ -47,6 +48,7 @@ interface AgentFormFieldsProps {
}
const SkillsFieldArray = () => {
+ const { t } = useTranslation();
const { control } = useFormContext
();
const { fields, append, remove } = useFieldArray({ control, name: "skills" });
@@ -57,14 +59,14 @@ const SkillsFieldArray = () => {
{({ value, onChange, ref, ...control }) => (
@@ -73,14 +75,16 @@ const SkillsFieldArray = () => {
{({ value, onChange, ref, ...control }) => (
@@ -89,15 +93,19 @@ const SkillsFieldArray = () => {
{({ value, onChange, ref, ...control }) => (
@@ -106,26 +114,28 @@ const SkillsFieldArray = () => {
{({ id, value, onChange }) => (
)}
-
+
{({ id, value, onChange }) => (
)}
@@ -138,19 +148,20 @@ const SkillsFieldArray = () => {
onClick={() => remove(index)}
>
- Remove Skill
+ {t("agents.formFields.skills.remove")}
))}
append({})}>
- Add Skill
+ {t("agents.formFields.skills.add")}
>
);
};
const StaticHeadersFieldArray = () => {
+ const { t } = useTranslation();
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({ control, name: "static_headers" });
@@ -158,25 +169,31 @@ const StaticHeadersFieldArray = () => {
<>
{fields.map((item, index) => (
-
+
{({ value, onChange, ref, ...control }) => (
)}
-
+
{({ value, onChange, ref, ...control }) => (
@@ -186,7 +203,7 @@ const StaticHeadersFieldArray = () => {
type="button"
variant="ghost"
size="icon"
- aria-label="Remove static header"
+ aria-label={t("agents.formFields.staticHeaders.remove")}
className="text-destructive hover:text-destructive/80"
onClick={() => remove(index)}
>
@@ -196,13 +213,14 @@ const StaticHeadersFieldArray = () => {
))}
append({})}>
- Add Static Header
+ {t("agents.formFields.staticHeaders.add")}
>
);
};
const AgentFormFields: React.FC = ({ panels, showAgentName = true, visiblePanels }) => {
+ const { t } = useTranslation();
const shouldShow = (key: string) => !visiblePanels || visiblePanels.includes(key);
return (
@@ -211,14 +229,14 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{({ value, onChange, ref, ...control }) => (
@@ -231,7 +249,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{shouldShow(AGENT_FORM_CONFIG.basic.key) && (
{AGENT_FORM_CONFIG.basic.fields.map((field) => (
@@ -239,8 +257,12 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
key={field.name}
name={field.name}
label={field.tooltip ? labelWithHint(field.label, field.tooltip) : field.label}
- description={field.helpText}
- rules={field.required ? { required: `Please enter ${field.label.toLowerCase()}` } : undefined}
+ description={field.helpTextKey ? t(field.helpTextKey) : undefined}
+ rules={
+ field.required
+ ? { required: t("agents.formFields.errors.enterField", { field: field.label.toLowerCase() }) }
+ : undefined
+ }
>
{({ value, onChange, ref, ...control }) => {
const text = typeof value === "string" ? value : "";
@@ -284,7 +306,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{shouldShow(AGENT_FORM_CONFIG.skills.key) && (
@@ -294,7 +316,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{shouldShow(AGENT_FORM_CONFIG.capabilities.key) && (
{AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
@@ -310,7 +332,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{shouldShow(AGENT_FORM_CONFIG.optional.key) && (
{AGENT_FORM_CONFIG.optional.fields.map((field) => (
@@ -334,7 +356,11 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
)}
{shouldShow(AGENT_FORM_CONFIG.cost.key) && (
-
+
)}
@@ -342,7 +368,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
{shouldShow(AGENT_FORM_CONFIG.litellm.key) && (
{AGENT_FORM_CONFIG.litellm.fields.map((field) => (
@@ -366,12 +392,16 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
)}
{shouldShow(AUTH_HEADERS_PANEL_KEY) && (
-
+
{labelWithHint(
- "Static Headers",
- "Headers always sent to the backend agent, regardless of the client request. Admin-configured, static wins on conflict.",
+ t("agents.formFields.authHeaders.staticHeaders"),
+ t("agents.formFields.authHeaders.staticHeadersHint"),
)}
@@ -382,8 +412,8 @@ const AgentFormFields: React.FC
= ({ panels, showAgentName
{({ id, value, onChange }) => (
@@ -391,7 +421,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
id={id}
value={Array.isArray(value) ? (value as string[]) : []}
onValueChange={onChange}
- placeholder="e.g. x-api-key, Authorization"
+ placeholder={t("agents.formFields.authHeaders.forwardHeadersPlaceholder")}
/>
)}
@@ -399,7 +429,7 @@ const AgentFormFields: React.FC = ({ panels, showAgentName
)}
{shouldShow(KILL_SWITCH_PANEL_KEY) && (
-
+
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx
index 68c0a6a702d..554116716ea 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx
@@ -14,6 +14,7 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { toast } from "@/lib/toast";
import { ArrowLeft } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { getAgentInfo, patchAgentCall, getAgentCreateMetadata, AgentCreateInfo } from "@/components/networking";
import { Agent } from "@/components/agents/types";
import { KeyResponse } from "@/components/key_team_helpers/key_list";
@@ -79,6 +80,7 @@ const DetailItem: React.FC<{ label: React.ReactNode; children: React.ReactNode }
);
const AgentInfoView: React.FC = ({ agentId, onClose, accessToken, isAdmin }) => {
+ const { t } = useTranslation();
const [agent, setAgent] = useState(null);
const [selectedKey, setSelectedKey] = useState(null);
const { data: keysData, isLoading: keysLoading, refetch: refetchAgentKeys } = useKeys(1, 100, { agentID: agentId });
@@ -127,7 +129,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
if (agentType === "a2a") {
form.reset(parseAgentForForm(data));
} else {
- const typeInfo = agentTypeMetadata.find((t) => t.agent_type === agentType);
+ const typeInfo = agentTypeMetadata.find((info) => info.agent_type === agentType);
if (typeInfo) {
form.reset({
...parseDynamicAgentForForm(data, typeInfo),
@@ -140,7 +142,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
}
} catch (error) {
console.error("Error fetching agent info:", error);
- toast.error("Failed to load agent information");
+ toast.error(t("agents.info.toasts.loadFailed"));
} finally {
setIsLoading(false);
}
@@ -151,7 +153,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
if (agent && agentTypeMetadata.length > 0) {
const agentType = detectAgentType(agent);
if (agentType !== "a2a") {
- const typeInfo = agentTypeMetadata.find((t) => t.agent_type === agentType);
+ const typeInfo = agentTypeMetadata.find((info) => info.agent_type === agentType);
if (typeInfo) {
form.reset({
...parseDynamicAgentForForm(agent, typeInfo),
@@ -163,7 +165,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
}
}, [agentTypeMetadata, agent]);
- const selectedAgentTypeInfo = agentTypeMetadata.find((t) => t.agent_type === detectedAgentType);
+ const selectedAgentTypeInfo = agentTypeMetadata.find((info) => info.agent_type === detectedAgentType);
const watchedFormValues = useWatch({ control: form.control });
const mcpSelection = useWatch({ control: form.control, name: "allowed_mcp_servers_and_groups" });
const mcpToolPermissions = useWatch({ control: form.control, name: "mcp_tool_permissions" });
@@ -249,12 +251,12 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
object_permission: buildMcpObjectPermission(values),
access_group_ids: values.access_group_ids ?? [],
});
- toast.success("Agent updated successfully");
+ toast.success(t("agents.info.toasts.updated"));
setIsEditing(false);
fetchAgentInfo();
} catch (error) {
console.error("Error updating agent:", error);
- toast.error("Failed to update agent");
+ toast.error(t("agents.info.toasts.updateFailed"));
} finally {
setIsSaving(false);
}
@@ -273,9 +275,9 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
if (!agent) {
return (
-
Agent not found
+
{t("agents.info.notFound")}
- Back to Agents List
+ {t("agents.info.backToList")}
);
@@ -297,7 +299,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
onChange={onChange}
inputRef={ref}
min={0}
- placeholder="Unlimited"
+ placeholder={t("agents.info.unlimited")}
/>
)}
@@ -314,7 +316,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
refetchAgentKeys();
}}
teams={null}
- backButtonText="Back to Agent"
+ backButtonText={t("agents.info.backToAgent")}
/>
);
}
@@ -324,20 +326,20 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
- Back to Agents
+ {t("agents.info.backToAgents")}
-
{agent.agent_name || "Unnamed Agent"}
+
{agent.agent_name || t("agents.info.unnamed")}
{agent.agent_id}
- Overview
+ {t("agents.info.tabs.overview")}
{isAdmin && (
- Settings
+ {t("agents.info.tabs.settings")}
)}
@@ -352,38 +354,62 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
isAdmin={isAdmin}
/>
- {agent.agent_id}
- {agent.agent_name}
- {agent.agent_card_params?.name || "-"}
- {agent.agent_card_params?.description || "-"}
- {agent.agent_card_params?.url || "-"}
- {agent.agent_card_params?.version || "-"}
- {agent.agent_card_params?.protocolVersion || "-"}
-
- {agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
+ {agent.agent_id}
+ {agent.agent_name}
+
+ {agent.agent_card_params?.name || "-"}
+
+
+ {agent.agent_card_params?.description || "-"}
+
+ {agent.agent_card_params?.url || "-"}
+
+ {agent.agent_card_params?.version || "-"}
+
+
+ {agent.agent_card_params?.protocolVersion || "-"}
+
+
+ {agent.agent_card_params?.capabilities?.streaming ? t("agents.info.yes") : t("agents.info.no")}
{agent.agent_card_params?.capabilities?.pushNotifications && (
- Yes
+ {t("agents.info.yes")}
)}
{agent.agent_card_params?.capabilities?.stateTransitionHistory && (
- Yes
+ {t("agents.info.yes")}
+ )}
+
+ {t("agents.info.skillsConfigured", { count: agent.agent_card_params?.skills?.length || 0 })}
+
+ {agent.litellm_params?.model && (
+ {agent.litellm_params.model}
)}
- {agent.agent_card_params?.skills?.length || 0} configured
- {agent.litellm_params?.model && {agent.litellm_params.model} }
{agent.litellm_params?.make_public !== undefined && (
- {agent.litellm_params.make_public ? "Yes" : "No"}
+
+ {agent.litellm_params.make_public ? t("agents.info.yes") : t("agents.info.no")}
+
)}
{agent.agent_card_params?.iconUrl && (
- {agent.agent_card_params.iconUrl}
+ {agent.agent_card_params.iconUrl}
)}
{agent.agent_card_params?.documentationUrl && (
- {agent.agent_card_params.documentationUrl}
+
+ {agent.agent_card_params.documentationUrl}
+
)}
- {agent.tpm_limit ?? "Unlimited"}
- {agent.rpm_limit ?? "Unlimited"}
- {agent.session_tpm_limit ?? "Unlimited"}
- {agent.session_rpm_limit ?? "Unlimited"}
-
+
+ {agent.tpm_limit ?? t("agents.info.unlimited")}
+
+
+ {agent.rpm_limit ?? t("agents.info.unlimited")}
+
+
+ {agent.session_tpm_limit ?? t("agents.info.unlimited")}
+
+
+ {agent.session_rpm_limit ?? t("agents.info.unlimited")}
+
+
{agent.access_group_ids?.length ? (
{agent.access_group_ids.map((accessGroupId) => (
@@ -391,11 +417,11 @@ const AgentInfoView: React.FC
= ({ agentId, onClose, accessT
))}
) : (
- "None"
+ t("agents.info.none")
)}
- {formatDate(agent.created_at)}
- {formatDate(agent.updated_at)}
+ {formatDate(agent.created_at)}
+ {formatDate(agent.updated_at)}
@@ -407,10 +433,10 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
(agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions).length > 0)) && (
-
MCP Tool Permissions
+
{t("agents.info.mcpPermissions.title")}
{agent.object_permission.mcp_servers && agent.object_permission.mcp_servers.length > 0 && (
-
+
{agent.object_permission.mcp_servers.map((serverId) => (
{mcpServerLabel(serverId)}
@@ -420,16 +446,18 @@ const AgentInfoView: React.FC
= ({ agentId, onClose, accessT
)}
{agent.object_permission.mcp_access_groups &&
agent.object_permission.mcp_access_groups.length > 0 && (
-
+
{agent.object_permission.mcp_access_groups.join(", ")}
)}
{agent.object_permission.mcp_toolsets && agent.object_permission.mcp_toolsets.length > 0 && (
- {agent.object_permission.mcp_toolsets.join(", ")}
+
+ {agent.object_permission.mcp_toolsets.join(", ")}
+
)}
{agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions).length > 0 && (
-
+
{Object.entries(agent.object_permission.mcp_tool_permissions).map(([serverId, tools]) => (
@@ -448,23 +476,27 @@ const AgentInfoView: React.FC
= ({ agentId, onClose, accessT
{agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
-
Skills
+
{t("agents.info.skillsSection.title")}
{agent.agent_card_params.skills.map((skill, index) => (
-
+
- ID: {skill.id}
+ {t("agents.info.skillsSection.id")} {skill.id}
- Description: {skill.description}
+ {t("agents.info.skillsSection.description")} {skill.description}
- Tags: {Array.isArray(skill.tags) ? skill.tags.join(", ") : skill.tags}
+ {t("agents.info.skillsSection.tags")} {" "}
+ {Array.isArray(skill.tags) ? skill.tags.join(", ") : skill.tags}
{skill.examples && skill.examples.length > 0 && (
- Examples: {" "}
+ {t("agents.info.skillsSection.examples")} {" "}
{Array.isArray(skill.examples) ? skill.examples.join(", ") : skill.examples}
)}
@@ -489,7 +521,7 @@ const AgentInfoView: React.FC
= ({ agentId, onClose, accessT
-
Agent Settings
+
{t("agents.info.settings.title")}
{!isEditing && (
{
@@ -497,7 +529,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
setIsEditing(true);
}}
>
- Edit Settings
+ {t("agents.info.settings.edit")}
)}
@@ -508,7 +540,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
) : (
- Click "Edit Settings" to modify agent configuration.
+ {t("agents.info.settings.editHint")}
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx
index f9485c2b1a7..c6846b9a071 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_virtual_keys.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useTranslation } from "react-i18next";
import { KeyRound } from "lucide-react";
import { KeyResponse } from "@/components/key_team_helpers/key_list";
import { Button } from "@/components/ui/button";
@@ -11,19 +12,22 @@ interface AgentVirtualKeysProps {
}
const AgentVirtualKeys: React.FC = ({ keys, isLoading, onKeyClick }) => {
+ const { t } = useTranslation();
return (
-
Virtual Keys
+
{t("agents.virtualKeys.title")}
{isLoading ? (
-
Loading keys...
+
{t("agents.virtualKeys.loading")}
) : keys.length === 0 ? (
-
No virtual key assigned to this agent.
+
{t("agents.virtualKeys.empty")}
) : (
{keys.map((key) => (
-
{key.key_alias || "Unnamed key"}
+
+ {key.key_alias || t("agents.virtualKeys.unnamed")}
+
{key.key_name &&
{key.key_name} }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx
index 85ffbffeab1..4ec37821989 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx
@@ -1,10 +1,13 @@
import React from "react";
+import type { TFunction } from "i18next";
+import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { FieldGroup } from "@/components/ui/field";
import { AgentCreateInfo, AgentCredentialFieldMetadata } from "@/components/networking";
import { PasswordInput } from "@/components/shared/PasswordInput";
+import i18n from "@/i18n/i18n";
import { AGENT_FORM_CONFIG, applyKillSwitchToPayload } from "./agent_config";
import CostConfigFields, { COST_FIELD_NAMES } from "./cost_config_fields";
import KillSwitchFormFields from "./KillSwitchFormFields";
@@ -32,12 +35,13 @@ export const unmountedDynamicFieldNames = (mountedPanels: readonly string[]): re
// validation rather than throwing during render and taking the whole form down with it.
const buildValidationPatternRule = (
field: AgentCredentialFieldMetadata,
+ t: TFunction,
): { value: RegExp; message: string } | undefined => {
if (!field.validation_pattern) return undefined;
try {
return {
value: new RegExp(field.validation_pattern),
- message: field.validation_message || `${field.label} looks incomplete or malformed`,
+ message: field.validation_message || t("agents.dynamicFields.errors.malformed", { field: field.label }),
};
} catch {
return undefined;
@@ -45,14 +49,15 @@ const buildValidationPatternRule = (
};
const CredentialField = ({ field }: { field: AgentCredentialFieldMetadata }) => {
- const patternRule = buildValidationPatternRule(field);
+ const { t } = useTranslation();
+ const patternRule = buildValidationPatternRule(field, t);
return (
@@ -108,14 +113,17 @@ const DynamicAgentFormFields: React.FC = ({ agentTy
{({ value, onChange, ref, ...control }) => (
@@ -124,14 +132,17 @@ const DynamicAgentFormFields: React.FC = ({ agentTy
{({ value, onChange, ref, ...control }) => (
@@ -144,10 +155,18 @@ const DynamicAgentFormFields: React.FC = ({ agentTy
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/kill_switch_config.ts b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/kill_switch_config.ts
index d59808bc1b3..c14b2b68d2b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/kill_switch_config.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/kill_switch_config.ts
@@ -1,3 +1,5 @@
+import type { ParseKeys, TFunction } from "i18next";
+import i18n from "@/i18n/i18n";
import type { components } from "@/lib/http/schema";
export type KillSwitchConfig = components["schemas"]["AgentKillSwitchConfig"];
@@ -6,11 +8,11 @@ export type KillSwitchMethod = NonNullable;
export type KillSwitchAuthType = KillSwitchAuth["type"] | "none";
export const KILL_SWITCH_METHODS: readonly KillSwitchMethod[] = ["POST", "PUT", "PATCH", "DELETE", "GET"];
-export const KILL_SWITCH_AUTH_TYPES: readonly { value: KillSwitchAuthType; label: string }[] = [
- { value: "none", label: "None" },
- { value: "bearer", label: "Bearer token" },
- { value: "api_key", label: "API key header" },
- { value: "basic", label: "Basic auth" },
+export const KILL_SWITCH_AUTH_TYPES: readonly { value: KillSwitchAuthType; labelKey: ParseKeys }[] = [
+ { value: "none", labelKey: "agents.killSwitchConfig.authTypes.none" },
+ { value: "bearer", labelKey: "agents.killSwitchConfig.authTypes.bearer" },
+ { value: "api_key", labelKey: "agents.killSwitchConfig.authTypes.apiKey" },
+ { value: "basic", labelKey: "agents.killSwitchConfig.authTypes.basic" },
];
export interface KeyValueFormValue {
@@ -61,12 +63,12 @@ export const parseKillSwitchBody = (text: string | undefined): Record;
};
-export const validateKillSwitchBody = (text: string | undefined): true | string => {
+export const validateKillSwitchBody = (text: string | undefined, t: TFunction = i18n.t.bind(i18n)): true | string => {
try {
parseKillSwitchBody(text);
return true;
} catch (error) {
- return error instanceof Error ? error.message : "Body must be valid JSON";
+ return error instanceof Error ? error.message : t("agents.killSwitchConfig.errors.invalidJson");
}
};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryDetailDrawer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryDetailDrawer.tsx
index 5c2fac2d5a9..ae88b42c906 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryDetailDrawer.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryDetailDrawer.tsx
@@ -1,6 +1,7 @@
"use client";
import React from "react";
+import { useTranslation } from "react-i18next";
import { MemoryRow } from "@/components/networking";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
@@ -25,6 +26,7 @@ function formatTimestamp(ts?: string): string {
}
export function MemoryDetailDrawer({ row, onClose }: MemoryDetailDrawerProps) {
+ const { t } = useTranslation();
return (
- {row ? {row.key} : "Memory"}
+
+ {row ? {row.key} : t("memory.detailDrawer.title")}
+
{row && (
- Memory ID
+ {t("memory.detailDrawer.memoryId")}
{row.memory_id}
- User ID
+ {t("memory.detailDrawer.userId")}
{row.user_id ?? "-"}
- Team ID
+ {t("memory.detailDrawer.teamId")}
{row.team_id ?? "-"}
-
Value
+
{t("memory.detailDrawer.value")}
{row.value}
{row.metadata !== undefined && row.metadata !== null && (
-
Metadata
+
{t("memory.detailDrawer.metadata")}
{JSON.stringify(row.metadata, null, 2)}
)}
- Created {formatTimestamp(row.created_at)}
- {row.created_by ? ` by ${row.created_by}` : ""}
+ {t("memory.detailDrawer.created", { time: formatTimestamp(row.created_at) })}
+ {row.created_by ? t("memory.detailDrawer.by", { name: row.created_by }) : ""}
·
- Updated {formatTimestamp(row.updated_at)}
- {row.updated_by ? ` by ${row.updated_by}` : ""}
+ {t("memory.detailDrawer.updated", { time: formatTimestamp(row.updated_at) })}
+ {row.updated_by ? t("memory.detailDrawer.by", { name: row.updated_by }) : ""}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryEditModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryEditModal.tsx
index dac209057f1..39d2f3b559d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryEditModal.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryEditModal.tsx
@@ -2,6 +2,8 @@
import { CircleHelp } from "lucide-react";
import React, { useEffect, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import i18n from "@/i18n/i18n";
import { z } from "zod";
import type { MemoryRow } from "@/components/networking";
@@ -15,8 +17,8 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
import { Button } from "@/components/ui/button";
const memorySchema = z.object({
- key: z.string().min(1, "Key is required"),
- value: z.string().min(1, "Value is required"),
+ key: z.string().min(1, { error: () => i18n.t("memory.editModal.errors.keyRequired") }),
+ value: z.string().min(1, { error: () => i18n.t("memory.editModal.errors.valueRequired") }),
metadata: z.string(),
});
@@ -43,6 +45,7 @@ interface MemoryEditModalProps {
}
export const MemoryEditModal: React.FC = ({ open, mode, initialRow, onClose, onSave }) => {
+ const { t } = useTranslation();
const form = useZodForm(memorySchema, { defaultValues: EMPTY_MEMORY, mode: "onChange" });
const [submitting, setSubmitting] = useState(false);
@@ -80,7 +83,11 @@ export const MemoryEditModal: React.FC = ({ open, mode, in
>
- {mode === "create" ? "Create memory" : `Edit ${initialRow?.key ?? ""}`}
+
+ {mode === "create"
+ ? t("memory.editModal.createTitle")
+ : t("memory.editModal.editTitle", { name: initialRow?.key ?? "" })}
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTable.tsx
index 3e37faafe15..6feb9795601 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTable.tsx
@@ -3,6 +3,7 @@
import { OnChangeFn, PaginationState } from "@tanstack/react-table";
import { Database } from "lucide-react";
import React, { useMemo } from "react";
+import { useTranslation } from "react-i18next";
import { MemoryRow } from "@/components/networking";
import { DataTable, DataTableToolbar } from "@/components/shared/DataTable";
@@ -26,18 +27,17 @@ interface MemoryTableProps {
}
function MemoryEmptyState({ hasActiveSearch }: { hasActiveSearch: boolean }) {
+ const { t } = useTranslation();
return (
- {hasActiveSearch ? "No matching memories" : "No memories stored yet"}
+ {hasActiveSearch ? t("memory.table.empty.titleSearch") : t("memory.table.empty.title")}
- {hasActiveSearch
- ? "No memories match your search."
- : "Memories your agents store under /v1/memory will appear here."}
+ {hasActiveSearch ? t("memory.table.empty.descriptionSearch") : t("memory.table.empty.description")}
);
@@ -58,10 +58,11 @@ export function MemoryTable({
onEditClick,
onDeleteClick,
}: MemoryTableProps) {
+ const { t } = useTranslation();
const columns = useMemo(() => {
- const columnDeps = { onViewClick, onEditClick, onDeleteClick };
+ const columnDeps = { onViewClick, onEditClick, onDeleteClick, t };
return getMemoryTableColumns(columnDeps);
- }, [onViewClick, onEditClick, onDeleteClick]);
+ }, [onViewClick, onEditClick, onDeleteClick, t]);
return (
}
size="compact"
toolbar={(table) => (
@@ -81,7 +82,7 @@ export function MemoryTable({
table={table}
searchValue={searchValue}
onSearchChange={onSearchChange}
- searchPlaceholder="Search by key prefix or memory ID…"
+ searchPlaceholder={t("memory.table.searchPlaceholder")}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
showViewOptions={false}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTableColumns.tsx
index 62b8ec624fa..4b43582abb2 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTableColumns.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryTableColumns.tsx
@@ -1,7 +1,9 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
+import type { TFunction } from "i18next";
import { Eye, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { MemoryRow } from "@/components/networking";
import { DateCell, IdCell, IdentityCell } from "@/components/shared/table_cells";
@@ -24,10 +26,11 @@ interface MemoryRowActionsProps {
}
function MemoryRowActions({ row, onViewClick, onEditClick, onDeleteClick }: MemoryRowActionsProps) {
+ const { t } = useTranslation();
return (
@@ -36,16 +39,16 @@ function MemoryRowActions({ row, onViewClick, onEditClick, onDeleteClick }: Memo
onViewClick(row)}>
- View
+ {t("memory.table.actions.view")}
onEditClick(row)}>
- Edit
+ {t("memory.table.actions.edit")}
onDeleteClick(row)}>
- Delete
+ {t("memory.table.actions.delete")}
@@ -56,18 +59,20 @@ export interface MemoryTableColumnsDeps {
onViewClick: (row: MemoryRow) => void;
onEditClick: (row: MemoryRow) => void;
onDeleteClick: (row: MemoryRow) => void;
+ t: TFunction;
}
export const getMemoryTableColumns = ({
onViewClick,
onEditClick,
onDeleteClick,
+ t,
}: MemoryTableColumnsDeps): ColumnDef[] => [
{
id: "memory_id",
accessorKey: "memory_id",
- meta: { title: "ID" },
- header: "ID",
+ meta: { title: t("memory.table.columns.id") },
+ header: t("memory.table.columns.id"),
size: 180,
enableSorting: false,
cell: ({ row }) => (
@@ -81,8 +86,8 @@ export const getMemoryTableColumns = ({
{
id: "key",
accessorKey: "key",
- meta: { title: "Name" },
- header: "Name",
+ meta: { title: t("memory.table.columns.name") },
+ header: t("memory.table.columns.name"),
size: 200,
enableSorting: false,
cell: ({ row }) => (
@@ -94,8 +99,8 @@ export const getMemoryTableColumns = ({
{
id: "value",
accessorKey: "value",
- meta: { title: "Preview" },
- header: "Preview",
+ meta: { title: t("memory.table.columns.preview") },
+ header: t("memory.table.columns.preview"),
enableSorting: false,
cell: ({ row }) => (
@@ -106,8 +111,8 @@ export const getMemoryTableColumns = ({
{
id: "user_id",
accessorKey: "user_id",
- meta: { title: "User ID" },
- header: "User ID",
+ meta: { title: t("memory.table.columns.userId") },
+ header: t("memory.table.columns.userId"),
size: 160,
enableSorting: false,
cell: ({ row }) => {
@@ -118,8 +123,8 @@ export const getMemoryTableColumns = ({
{
id: "team_id",
accessorKey: "team_id",
- meta: { title: "Team ID" },
- header: "Team ID",
+ meta: { title: t("memory.table.columns.teamId") },
+ header: t("memory.table.columns.teamId"),
size: 160,
enableSorting: false,
cell: ({ row }) => {
@@ -130,8 +135,8 @@ export const getMemoryTableColumns = ({
{
id: "updated_at",
accessorKey: "updated_at",
- meta: { title: "Updated" },
- header: "Updated",
+ meta: { title: t("memory.table.columns.updated") },
+ header: t("memory.table.columns.updated"),
size: 170,
enableSorting: false,
cell: ({ row }) => ,
@@ -139,7 +144,7 @@ export const getMemoryTableColumns = ({
{
id: "actions",
meta: { className: "text-right", headerClassName: "text-right" },
- header: () => Actions ,
+ header: () => {t("memory.table.columns.actions")} ,
size: 64,
enableSorting: false,
enableHiding: false,
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryView.tsx
index 58d4e42aa96..cf5191da864 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/_components/MemoryView.tsx
@@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { PaginationState } from "@tanstack/react-table";
import { Plus } from "lucide-react";
import React, { useCallback, useMemo, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
import { MemoryRow, createMemory, deleteMemory, fetchMemoryList, updateMemory } from "@/components/networking";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
@@ -25,6 +26,7 @@ interface MemoryViewProps {
const DEFAULT_PAGE_SIZE = 50;
export const MemoryView: React.FC = ({ accessToken }) => {
+ const { t } = useTranslation();
const [searchInput, setSearchInput] = useState("");
const [debouncedSearch] = useDebouncedValue(searchInput, { wait: DEBOUNCE_WAIT_MS });
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE });
@@ -72,11 +74,11 @@ export const MemoryView: React.FC = ({ accessToken }) => {
return createMemory(accessToken, args);
},
onSuccess: (row) => {
- toast.success(`Created ${row.key}`);
+ toast.success(t("memory.view.toasts.created", { name: row.key }));
invalidateList();
},
onError: (err: Error) => {
- toast.error(`Save failed: ${err.message}`);
+ toast.error(t("memory.view.toasts.saveFailed", { error: err.message }));
},
});
@@ -87,11 +89,11 @@ export const MemoryView: React.FC = ({ accessToken }) => {
return updateMemory(accessToken, key, payload);
},
onSuccess: (row) => {
- toast.success(`Updated ${row.key}`);
+ toast.success(t("memory.view.toasts.updated", { name: row.key }));
invalidateList();
},
onError: (err: Error) => {
- toast.error(`Save failed: ${err.message}`);
+ toast.error(t("memory.view.toasts.saveFailed", { error: err.message }));
},
});
@@ -101,11 +103,11 @@ export const MemoryView: React.FC = ({ accessToken }) => {
return deleteMemory(accessToken, key).then(() => key);
},
onSuccess: (key) => {
- toast.success(`Deleted ${key}`);
+ toast.success(t("memory.view.toasts.deleted", { name: key }));
invalidateList();
},
onError: (err: Error) => {
- toast.error(`Delete failed: ${err.message}`);
+ toast.error(t("memory.view.toasts.deleteFailed", { error: err.message }));
},
});
@@ -147,7 +149,7 @@ export const MemoryView: React.FC = ({ accessToken }) => {
try {
metadataPayload = JSON.parse(metadataText);
} catch {
- toast.error("Metadata must be valid JSON (or leave empty).");
+ toast.error(t("memory.view.toasts.invalidMetadata"));
return false;
}
}
@@ -178,18 +180,23 @@ export const MemoryView: React.FC = ({ accessToken }) => {
-
Memory
+
{t("memory.view.title")}
- Inspect what your agents have stored under{" "}
-
- /v1/memory
-
- . Scoped to memories visible to your user / team (admins see all).
+
+ /v1/memory
+
+ ),
+ }}
+ />
setIsCreateOpen(true)}>
- New memory
+ {t("memory.view.newMemory")}
@@ -228,16 +235,16 @@ export const MemoryView: React.FC
= ({ accessToken }) => {
{/* Delete confirmation modal */}
;
+ return ;
}
return ;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx
index e810c3622d1..9beb52f7b40 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx
@@ -3,6 +3,7 @@
import { SortingState } from "@tanstack/react-table";
import { Inbox } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
import { DataTable } from "@/components/shared/DataTable";
import { modelHubCall, PromptSpec } from "@/components/networking";
@@ -22,13 +23,14 @@ interface PromptTableProps {
const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }];
function EmptyState() {
+ const { t } = useTranslation();
return (
-
No prompts yet
-
Add a prompt to start managing reusable templates.
+
{t("prompts.table.empty.title")}
+
{t("prompts.table.empty.description")}
);
}
@@ -41,6 +43,7 @@ const PromptTable: React.FC = ({
accessToken,
isAdmin,
}) => {
+ const { t } = useTranslation();
const [sorting, setSorting] = useState(DEFAULT_SORTING);
const [modelHubData, setModelHubData] = useState>(new Map());
@@ -66,8 +69,8 @@ const PromptTable: React.FC = ({
}, [accessToken]);
const columns = useMemo(
- () => getPromptTableColumns({ modelHubData, isAdmin, onPromptClick, onDeleteClick }),
- [modelHubData, isAdmin, onPromptClick, onDeleteClick],
+ () => getPromptTableColumns({ modelHubData, isAdmin, onPromptClick, onDeleteClick, t }),
+ [modelHubData, isAdmin, onPromptClick, onDeleteClick, t],
);
return (
@@ -82,7 +85,7 @@ const PromptTable: React.FC = ({
sorting={sorting}
onSortingChange={setSorting}
isLoading={isLoading}
- loadingMessage="Loading prompts…"
+ loadingMessage={t("prompts.table.loading")}
noDataMessage={ }
size="compact"
/>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTableColumns.tsx
index db0392bbd6e..a7e1cfcaccb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTableColumns.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTableColumns.tsx
@@ -1,7 +1,9 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
+import type { TFunction } from "i18next";
import { Copy, MoreHorizontal, Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { CellTooltip, DateCell, IdentityCell, StatusBadge, StatusTone } from "@/components/shared/table_cells";
@@ -69,10 +71,11 @@ interface PromptRowActionsProps {
}
function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsProps) {
+ const { t } = useTranslation();
return (
@@ -81,10 +84,10 @@ function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsPr
void copyToClipboard(prompt.prompt_id, "Prompt ID copied")}
+ onClick={() => void copyToClipboard(prompt.prompt_id, t("prompts.table.rowActions.idCopied"))}
>
- Copy prompt ID
+ {t("prompts.table.rowActions.copyId")}
{isAdmin && (
<>
@@ -95,13 +98,13 @@ function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsPr
onClick={() =>
onDeleteClick?.(
prompt.prompt_id,
- prompt.prompt_id || "Unknown Prompt",
+ prompt.prompt_id || t("prompts.table.rowActions.unknownPrompt"),
prompt.environment || "development",
)
}
>
- Delete
+ {t("prompts.table.rowActions.delete")}
>
)}
@@ -115,6 +118,7 @@ interface PromptTableColumnsDeps {
isAdmin: boolean;
onPromptClick?: (id: string, environment: string) => void;
onDeleteClick?: (id: string, name: string, environment: string) => void;
+ t: TFunction;
}
export const getPromptTableColumns = ({
@@ -122,12 +126,13 @@ export const getPromptTableColumns = ({
isAdmin,
onPromptClick,
onDeleteClick,
+ t,
}: PromptTableColumnsDeps): ColumnDef[] => [
{
id: "prompt_id",
accessorKey: "prompt_id",
- meta: { title: "Prompt ID" },
- header: ({ column }) => ,
+ meta: { title: t("prompts.table.columns.promptId") },
+ header: ({ column }) => ,
size: 220,
enableSorting: true,
cell: ({ row }) => (
@@ -145,8 +150,8 @@ export const getPromptTableColumns = ({
},
{
id: "model",
- meta: { title: "Model" },
- header: "Model",
+ meta: { title: t("prompts.table.columns.model") },
+ header: t("prompts.table.columns.model"),
size: 200,
enableSorting: false,
cell: ({ row }) => ,
@@ -155,8 +160,8 @@ export const getPromptTableColumns = ({
id: "created_at",
accessorKey: "created_at",
sortingFn: "datetime",
- meta: { title: "Created At" },
- header: ({ column }) => ,
+ meta: { title: t("prompts.table.columns.createdAt") },
+ header: ({ column }) => ,
size: 160,
enableSorting: true,
cell: ({ row }) => ,
@@ -165,8 +170,8 @@ export const getPromptTableColumns = ({
id: "updated_at",
accessorKey: "updated_at",
sortingFn: "datetime",
- meta: { title: "Updated At" },
- header: ({ column }) => ,
+ meta: { title: t("prompts.table.columns.updatedAt") },
+ header: ({ column }) => ,
size: 160,
enableSorting: true,
cell: ({ row }) => ,
@@ -174,8 +179,8 @@ export const getPromptTableColumns = ({
{
id: "environment",
accessorKey: "environment",
- meta: { title: "Environment", skeleton: "badge" },
- header: "Environment",
+ meta: { title: t("prompts.table.columns.environment"), skeleton: "badge" },
+ header: t("prompts.table.columns.environment"),
size: 130,
enableSorting: false,
cell: ({ row }) => {
@@ -186,8 +191,8 @@ export const getPromptTableColumns = ({
{
id: "created_by",
accessorKey: "created_by",
- meta: { title: "Created By" },
- header: "Created By",
+ meta: { title: t("prompts.table.columns.createdBy") },
+ header: t("prompts.table.columns.createdBy"),
size: 160,
enableSorting: false,
cell: ({ row }) => {
@@ -209,8 +214,8 @@ export const getPromptTableColumns = ({
{
id: "prompt_type",
accessorKey: "prompt_info.prompt_type",
- meta: { title: "Type" },
- header: "Type",
+ meta: { title: t("prompts.table.columns.type") },
+ header: t("prompts.table.columns.type"),
size: 140,
enableSorting: false,
cell: ({ row }) => {
@@ -225,7 +230,7 @@ export const getPromptTableColumns = ({
{
id: "actions",
meta: { className: "text-right", headerClassName: "text-right" },
- header: () => Actions ,
+ header: () => {t("prompts.table.columns.actions")} ,
size: 64,
enableSorting: false,
enableHiding: false,
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
index b12f5fdef4d..954d897de9c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/add_prompt_form.tsx
@@ -1,8 +1,10 @@
import React, { useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
import { Upload as UploadIcon, X } from "lucide-react";
import { z } from "zod";
import { convertPromptFileToJson, createPromptCall } from "@/components/networking";
import { toast } from "@/lib/toast";
+import i18n from "@/i18n/i18n";
import { Field, FieldDescription, FieldGroup, FieldSeparator, FieldTitle } from "@/components/ui/field";
import { FormField } from "@/components/shared/form/FormField";
import { Button } from "@/components/ui/button";
@@ -36,8 +38,8 @@ const PROMPT_INTEGRATION_OPTIONS = [{ label: "dotprompt", value: "dotprompt" }];
const addPromptSchema = z.object({
prompt_id: z
.string()
- .min(1, "Please enter a prompt ID")
- .regex(/^[a-zA-Z0-9_-]+$/, "Prompt ID can only contain letters, numbers, underscores, and hyphens"),
+ .min(1, { error: () => i18n.t("prompts.addForm.errors.idRequired") })
+ .regex(/^[a-zA-Z0-9_-]+$/, { error: () => i18n.t("prompts.addForm.errors.idInvalid") }),
prompt_integration: z.string(),
});
@@ -46,6 +48,7 @@ type AddPromptFormValues = z.infer;
const EMPTY_VALUES: AddPromptFormValues = { prompt_id: "", prompt_integration: "dotprompt" };
const AddPromptForm: React.FC = ({ visible, onClose, accessToken, onSuccess }) => {
+ const { t } = useTranslation();
const form = useZodForm(addPromptSchema, { defaultValues: EMPTY_VALUES });
const [loading, setLoading] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
@@ -70,7 +73,7 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
const picked = event.target.files?.[0];
if (!picked) return;
if (!picked.name.endsWith(".prompt")) {
- toast.fromError("Please upload a .prompt file");
+ toast.fromError(t("prompts.addForm.toasts.fileRequired"));
clearSelectedFile();
return;
}
@@ -104,21 +107,21 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
};
} catch (conversionError) {
console.error("Error converting prompt file:", conversionError);
- toast.fromError("Failed to convert prompt file to JSON");
+ toast.fromError(t("prompts.addForm.toasts.convertFailed"));
return null;
}
};
const handleSubmit = async (values: AddPromptFormValues) => {
if (!accessToken) {
- toast.fromError("Access token is required");
+ toast.fromError(t("prompts.addForm.toasts.tokenRequired"));
return;
}
const isDotprompt = promptIntegration === "dotprompt";
if (isDotprompt && !selectedFile) {
- toast.fromError("Please upload a .prompt file");
+ toast.fromError(t("prompts.addForm.toasts.fileRequired"));
return;
}
@@ -134,12 +137,12 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
try {
await createPromptCall(accessToken, promptData);
- toast.success("Prompt created successfully!");
+ toast.success(t("prompts.addForm.toasts.created"));
handleCancel();
onSuccess();
} catch (createError) {
console.error("Error creating prompt:", createError);
- toast.fromError("Failed to create prompt");
+ toast.fromError(t("prompts.addForm.toasts.createFailed"));
} finally {
setLoading(false);
}
@@ -149,17 +152,17 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
!open && handleCancel()}>
- Add New Prompt
+ {t("prompts.addForm.title")}
event.preventDefault()} noValidate>
-
+
{({ ref, ...field }) => (
-
+
)}
-
+
{({ id, value, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
@@ -180,25 +183,25 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
<>
- Prompt File
+ {t("prompts.addForm.fields.file")}
fileInputRef.current?.click()}>
- Select .prompt File
+ {t("prompts.addForm.fields.selectFile")}
{selectedFile && (
-
Selected: {selectedFile.name}
+
{t("prompts.addForm.fields.selected", { name: selectedFile.name })}
@@ -206,7 +209,7 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
)}
- Upload a .prompt file that follows the Dotprompt specification
+ {t("prompts.addForm.fields.fileDescription")}
>
)}
@@ -214,11 +217,11 @@ const AddPromptForm: React.FC = ({ visible, onClose, accessT
- Cancel
+ {t("prompts.addForm.cancel")}
void form.handleSubmit(handleSubmit)()}>
{loading && }
- Create Prompt
+ {t("prompts.addForm.submit")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
index 2d8d905c480..b6e795f7a47 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
@@ -1,4 +1,6 @@
import React, { useState, useEffect } from "react";
+import type { ParseKeys } from "i18next";
+import { useTranslation } from "react-i18next";
import { Plus, Upload } from "lucide-react";
import { getPromptsList, PromptSpec, ListPromptsResponse, deletePromptCall } from "@/components/networking";
@@ -20,16 +22,16 @@ import {
} from "@/components/ui/alert-dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-const ALL_ENVIRONMENTS_LABEL = "All Environments";
+const ALL_ENVIRONMENTS_LABEL_KEY: ParseKeys = "prompts.panel.environments.all";
-const ENVIRONMENT_OPTIONS = [
- { label: "Development", value: "development" },
- { label: "Staging", value: "staging" },
- { label: "Production", value: "production" },
+const ENVIRONMENT_OPTIONS: { labelKey: ParseKeys; value: string }[] = [
+ { labelKey: "prompts.panel.environments.development", value: "development" },
+ { labelKey: "prompts.panel.environments.staging", value: "staging" },
+ { labelKey: "prompts.panel.environments.production", value: "production" },
];
// SelectValue falls back to the raw value unless the root can map it to a label.
-const ENVIRONMENT_ITEMS = [{ label: ALL_ENVIRONMENTS_LABEL, value: null }, ...ENVIRONMENT_OPTIONS];
+const ENVIRONMENT_ITEMS = [{ labelKey: ALL_ENVIRONMENTS_LABEL_KEY, value: null }, ...ENVIRONMENT_OPTIONS];
interface PromptsProps {
accessToken: string | null;
@@ -37,6 +39,7 @@ interface PromptsProps {
}
const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
+ const { t } = useTranslation();
const [promptsList, setPromptsList] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedEnvironment, setSelectedEnvironment] = useState(undefined);
@@ -123,11 +126,13 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
setIsDeleting(true);
try {
await deletePromptCall(accessToken, promptToDelete.id, promptToDelete.environment);
- toast.success(`Prompt "${promptToDelete.name}" deleted successfully from ${promptToDelete.environment}`);
+ toast.success(
+ t("prompts.panel.toasts.deleted", { name: promptToDelete.name, environment: promptToDelete.environment }),
+ );
fetchPrompts(); // Refresh the list
} catch (error) {
console.error("Error deleting prompt:", error);
- toast.fromError("Failed to delete prompt");
+ toast.fromError(t("prompts.panel.toasts.deleteFailed"));
} finally {
setIsDeleting(false);
setPromptToDelete(null);
@@ -165,28 +170,28 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
<>
- Add New Prompt
+ {t("prompts.panel.addNew")}
- Upload .prompt File
+ {t("prompts.panel.uploadFile")}
>
)}
({ label: t(item.labelKey), value: item.value }))}
value={selectedEnvironment ?? null}
onValueChange={(value) => setSelectedEnvironment((value as string | null) ?? undefined)}
>
-
+
- {ALL_ENVIRONMENTS_LABEL}
+ {t(ALL_ENVIRONMENTS_LABEL_KEY)}
{ENVIRONMENT_OPTIONS.map((option) => (
- {option.label}
+ {t(option.labelKey)}
))}
@@ -220,16 +225,18 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
>
- Delete Prompt
+ {t("prompts.panel.deleteDialog.title")}
- Are you sure you want to delete the {promptToDelete.environment} copy of prompt: {promptToDelete.name}?
- This action cannot be undone.
+ {t("prompts.panel.deleteDialog.description", {
+ environment: promptToDelete.environment,
+ name: promptToDelete.name,
+ })}
- Cancel
+ {t("prompts.panel.deleteDialog.cancel")}
- Delete
+ {t("prompts.panel.deleteDialog.confirm")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DeveloperMessageCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DeveloperMessageCard.tsx
index 27c8907012c..ce417d00c62 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DeveloperMessageCard.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DeveloperMessageCard.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import VariableTextArea from "../variable_textarea";
@@ -8,16 +9,17 @@ interface DeveloperMessageCardProps {
}
const DeveloperMessageCard: React.FC = ({ value, onChange }) => {
+ const { t } = useTranslation();
return (
- Developer message
- Optional system instructions for the model
+ {t("prompts.developerMessage.title")}
+ {t("prompts.developerMessage.description")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DotpromptViewTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DotpromptViewTab.tsx
index 1be456c9b09..b1e1f78bbd3 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DotpromptViewTab.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/DotpromptViewTab.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useTranslation } from "react-i18next";
import { PromptType } from "./types";
import { convertToDotPrompt } from "./utils";
@@ -7,13 +8,14 @@ interface DotpromptViewTabProps {
}
const DotpromptViewTab: React.FC = ({ prompt }) => {
+ const { t } = useTranslation();
const dotpromptContent = convertToDotPrompt(prompt);
return (
-
Generated .prompt file
-
This is the dotprompt format that will be saved to the database
+
{t("prompts.dotpromptView.title")}
+
{t("prompts.dotpromptView.description")}
{dotpromptContent}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ModelConfigCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ModelConfigCard.tsx
index e9b526c9703..83264e86d1a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ModelConfigCard.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ModelConfigCard.tsx
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { SettingsIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
import ModelSelector from "@/components/common_components/ModelSelector";
interface ModelConfigCardProps {
@@ -24,6 +25,7 @@ const ModelConfigCard: React.FC
= ({
onTemperatureChange,
onMaxTokensChange,
}) => {
+ const { t } = useTranslation();
const [showConfig, setShowConfig] = useState(false);
return (
@@ -34,19 +36,19 @@ const ModelConfigCard: React.FC = ({
setShowConfig(!showConfig)} className="gap-2">
- Parameters
+ {t("prompts.modelConfig.parameters")}
- Model Parameters
+ {t("prompts.modelConfig.title")}
- Temperature
+ {t("prompts.modelConfig.temperature")}
= ({
- Max Tokens
+ {t("prompts.modelConfig.maxTokens")}
= ({
environment,
proxySettings,
}) => {
+ const { t } = useTranslation();
const syntaxTheme = useSyntaxTheme(coy);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedLanguage, setSelectedLanguage] = useState<"curl" | "python" | "javascript">("curl");
@@ -252,18 +254,18 @@ main();`;
<>
- Get Code
+ {t("prompts.codeSnippets.trigger")}
!open && handleCancel()}>
- Generated Code
+ {t("prompts.codeSnippets.title")}
- Language
+ {t("prompts.codeSnippets.language")}
{
navigator.clipboard.writeText(generatedCode);
- toast.success("Copied to clipboard!");
+ toast.success(t("prompts.codeSnippets.copied"));
}}
>
- Copy to Clipboard
+ {t("prompts.codeSnippets.copy")}
setSelectedTab(String(value))}>
-
- Basic
- With Messages
- With Version
+
+ {t("prompts.codeSnippets.tabs.basic")}
+ {t("prompts.codeSnippets.tabs.messages")}
+ {t("prompts.codeSnippets.tabs.version")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
index 0f001979c28..c0e28631ade 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { ArrowLeftIcon, SaveIcon, ClockIcon, LoaderCircleIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
import PromptCodeSnippets from "./PromptCodeSnippets";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -7,9 +8,9 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const ENVIRONMENT_ITEMS = [
- { value: "development", label: "Development" },
- { value: "staging", label: "Staging" },
- { value: "production", label: "Production" },
+ { value: "development", labelKey: "prompts.editorHeader.environments.development" },
+ { value: "staging", labelKey: "prompts.editorHeader.environments.staging" },
+ { value: "production", labelKey: "prompts.editorHeader.environments.production" },
] as const;
interface PromptEditorHeaderProps {
@@ -48,15 +49,16 @@ const PromptEditorHeader: React.FC
= ({
environment,
onEnvironmentChange,
}) => {
+ const { t } = useTranslation();
return (
= ({
{editMode && onShowHistory && (
- History
+ {t("prompts.editorHeader.history")}
)}
{isSaving ? : }
- {editMode ? "Update" : "Save"}
+ {editMode ? t("prompts.editorHeader.update") : t("prompts.editorHeader.save")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptMessagesCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptMessagesCard.tsx
index e69cae030c0..04722d372f4 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptMessagesCard.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptMessagesCard.tsx
@@ -1,4 +1,5 @@
import React, { useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
import { PlusIcon, TrashIcon, GripVerticalIcon } from "lucide-react";
import VariableTextArea from "../variable_textarea";
import { Message } from "./types";
@@ -7,9 +8,9 @@ import { Card } from "@/components/ui/card";
import { Select as ShadcnSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const ROLE_ITEMS = [
- { value: "user", label: "User" },
- { value: "assistant", label: "Assistant" },
- { value: "system", label: "System" },
+ { value: "user", labelKey: "prompts.messagesCard.roles.user" },
+ { value: "assistant", labelKey: "prompts.messagesCard.roles.assistant" },
+ { value: "system", labelKey: "prompts.messagesCard.roles.system" },
] as const;
interface PromptMessagesCardProps {
@@ -27,6 +28,7 @@ const PromptMessagesCard: React.FC = ({
onRemoveMessage,
onMoveMessage,
}) => {
+ const { t } = useTranslation();
const [draggedIndex, setDraggedIndex] = useState(null);
const [dragOverIndex, setDragOverIndex] = useState(null);
@@ -56,9 +58,15 @@ const PromptMessagesCard: React.FC = ({
return (
-
Prompt messages
+
{t("prompts.messagesCard.title")}
- Use {"{{variable}}"} syntax for template variables
+ ,
+ variable: <>{"{{variable}}"}>,
+ }}
+ />
@@ -76,21 +84,21 @@ const PromptMessagesCard: React.FC
= ({
>
({ value: item.value, label: t(item.labelKey) }))}
value={message.role}
onValueChange={(value) => onUpdateMessage(index, "role", String(value))}
>
{ROLE_ITEMS.map((item) => (
- {item.label}
+ {t(item.labelKey)}
))}
@@ -100,7 +108,7 @@ const PromptMessagesCard: React.FC = ({
onRemoveMessage(index)}
>
@@ -116,7 +124,7 @@ const PromptMessagesCard: React.FC = ({
value={message.content}
onChange={(value) => onUpdateMessage(index, "content", value)}
rows={3}
- placeholder="Enter prompt content..."
+ placeholder={t("prompts.messagesCard.contentPlaceholder")}
/>
@@ -124,7 +132,7 @@ const PromptMessagesCard: React.FC = ({
- Add message
+ {t("prompts.messagesCard.add")}
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PublishModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PublishModal.tsx
index e37efd3464a..8aeb52efab1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PublishModal.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PublishModal.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { LoaderCircleIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -28,36 +29,35 @@ const PublishModal: React.FC = ({
onPublish,
onCancel,
}) => {
+ const { t } = useTranslation();
return (
!open && onCancel()}>
- Publish Prompt
- Published prompts are versioned and can be used in API calls.
+ {t("prompts.publishModal.title")}
+ {t("prompts.publishModal.description")}
- Name
+ {t("prompts.publishModal.nameLabel")}
onNameChange(e.target.value)}
- placeholder="Enter prompt name"
+ placeholder={t("prompts.publishModal.namePlaceholder")}
onKeyDown={(event) => event.key === "Enter" && onPublish()}
autoFocus
/>
-
- Published prompts can be used in API calls and are versioned for easy tracking.
-
+
{t("prompts.publishModal.hint")}
- Cancel
+ {t("prompts.publishModal.cancel")}
{isSaving && }
- Publish
+ {t("prompts.publishModal.publish")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ToolsCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ToolsCard.tsx
index c8c8f3e68fa..17f22abfe13 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ToolsCard.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/ToolsCard.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { PlusIcon, TrashIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { Tool } from "./types";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -12,17 +13,18 @@ interface ToolsCardProps {
}
const ToolsCard: React.FC = ({ tools, onAddTool, onEditTool, onRemoveTool }) => {
+ const { t } = useTranslation();
return (
-
Tools
+
{t("prompts.toolsCard.title")}
- Add
+ {t("prompts.toolsCard.add")}
{tools.length === 0 ? (
- No tools added
+ {t("prompts.toolsCard.empty")}
) : (
{tools.map((tool, index) => (
@@ -33,12 +35,12 @@ const ToolsCard: React.FC = ({ tools, onAddTool, onEditTool, onR
onEditTool(index)}>
- Edit
+ {t("prompts.toolsCard.edit")}
onRemoveTool(index)}
>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.tsx
index 64bc7755694..9449317d5bf 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/VersionHistorySidePanel.tsx
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { XIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
interface VersionHistorySidePanelProps {
isOpen: boolean;
@@ -22,6 +23,7 @@ const VersionHistorySidePanel: React.FC = ({
activeVersionId,
onSelectVersion,
}) => {
+ const { t } = useTranslation();
const [versions, setVersions] = useState([]);
const [loading, setLoading] = useState(false);
@@ -90,23 +92,23 @@ const VersionHistorySidePanel: React.FC = ({
>
- Close
+ {t("prompts.versionHistory.close")}
- Version History
+ {t("prompts.versionHistory.title")}
{loading ? (
-
+
) : versions.length === 0 ? (
-
No version history available.
+
{t("prompts.versionHistory.empty")}
) : (
{versions.map((item, index) => {
@@ -138,15 +140,17 @@ const VersionHistorySidePanel: React.FC
= ({
{getVersionNumber(item)}
- {index === 0 && Latest }
+ {index === 0 && {t("prompts.versionHistory.latest")} }
- {isSelected &&
Active }
+ {isSelected &&
{t("prompts.versionHistory.active")} }
{formatDate(item.created_at)}
- {item.prompt_info?.prompt_type === "db" ? "Saved to Database" : "Config Prompt"}
+ {item.prompt_info?.prompt_type === "db"
+ ? t("prompts.versionHistory.savedToDatabase")
+ : t("prompts.versionHistory.configPrompt")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/EmptyState.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/EmptyState.tsx
index 5b381427988..962a946f483 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/EmptyState.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/EmptyState.tsx
@@ -1,18 +1,20 @@
import React from "react";
import { Bot } from "lucide-react";
+import { useTranslation } from "react-i18next";
interface EmptyStateProps {
hasVariables: boolean;
}
const EmptyState: React.FC = ({ hasVariables }) => {
+ const { t } = useTranslation();
return (
{hasVariables
- ? "Fill in the variables above, then type a message to start testing"
- : "Type a message below to start testing your prompt"}
+ ? t("prompts.conversation.emptyState.withVariables")
+ : t("prompts.conversation.emptyState.withoutVariables")}
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageInput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageInput.tsx
index d2c47a2dd4d..35943d6283e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageInput.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageInput.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { ArrowUp } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
@@ -22,6 +23,7 @@ const MessageInput: React.FC = ({
onKeyDown,
onCancel,
}) => {
+ const { t } = useTranslation();
return (
@@ -29,7 +31,7 @@ const MessageInput: React.FC
= ({
value={inputMessage}
onChange={(e) => onInputChange(e.target.value)}
onKeyDown={onKeyDown}
- placeholder="Type your message... (Shift+Enter for new line)"
+ placeholder={t("prompts.conversation.input.placeholder")}
disabled={isLoading}
rows={1}
className="field-sizing-content max-h-24 min-h-8 flex-1 resize-none overflow-y-auto border-0 bg-transparent px-0 py-1 text-sm shadow-none focus-visible:ring-0"
@@ -41,7 +43,7 @@ const MessageInput: React.FC = ({
onClick={onSend}
disabled={isDisabled}
className="ml-2 shrink-0 rounded-full"
- aria-label="Send message"
+ aria-label={t("prompts.conversation.input.send")}
>
@@ -49,7 +51,7 @@ const MessageInput: React.FC = ({
{isLoading && (
- Cancel
+ {t("prompts.conversation.input.cancel")}
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageList.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageList.tsx
index 48d6697a052..c5e4498a722 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageList.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/MessageList.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { Loader2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
import EmptyState from "./EmptyState";
import MessageBubble from "./MessageBubble";
import { Message } from "./types";
@@ -12,6 +13,7 @@ interface MessageListProps {
}
const MessageList: React.FC
= ({ messages, isLoading, hasVariables, messagesEndRef }) => {
+ const { t } = useTranslation();
return (
{messages.length === 0 &&
}
@@ -22,7 +24,10 @@ const MessageList: React.FC
= ({ messages, isLoading, hasVaria
{isLoading && (
-
+
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableInput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableInput.tsx
index 96397ef4afa..64a8b790ade 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableInput.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableInput.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
interface VariableInputProps {
@@ -8,13 +9,14 @@ interface VariableInputProps {
}
const VariableInput: React.FC = ({ extractedVariables, variables, onVariableChange }) => {
+ const { t } = useTranslation();
if (extractedVariables.length === 0) {
return null;
}
return (
-
Fill in template variables to start testing
+
{t("prompts.conversation.variables.title")}
{extractedVariables.map((varName) => (
@@ -26,7 +28,7 @@ const VariableInput: React.FC = ({ extractedVariables, varia
onVariableChange(varName, e.target.value)}
- placeholder={`Enter value for ${varName}`}
+ placeholder={t("prompts.conversation.variables.placeholder", { name: varName })}
/>
))}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableWarning.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableWarning.tsx
index b4b710e37a0..4443ca07baa 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableWarning.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/VariableWarning.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useTranslation } from "react-i18next";
interface VariableWarningProps {
extractedVariables: string[];
@@ -6,6 +7,7 @@ interface VariableWarningProps {
}
const VariableWarning: React.FC
= ({ extractedVariables, variables }) => {
+ const { t } = useTranslation();
const missingVariables = extractedVariables.filter(
(varName) => !variables[varName] || variables[varName].trim() === "",
);
@@ -19,9 +21,11 @@ const VariableWarning: React.FC = ({ extractedVariables, v
⚠️
-
Please fill in all template variables above
+
{t("prompts.conversation.warning.title")}
- Missing: {missingVariables.map((varName) => `{{${varName}}}`).join(", ")}
+ {t("prompts.conversation.warning.missing", {
+ variables: missingVariables.map((varName) => `{{${varName}}}`).join(", "),
+ })}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/index.tsx
index e3cc15bf48b..4f53f4189cf 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/index.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { ConversationPanelProps } from "./types";
import { useConversation } from "./useConversation";
@@ -9,6 +10,7 @@ import VariableWarning from "./VariableWarning";
import MessageInput from "./MessageInput";
const ConversationPanel: React.FC = ({ prompt, accessToken }) => {
+ const { t } = useTranslation();
const {
isLoading,
messages,
@@ -40,7 +42,7 @@ const ConversationPanel: React.FC = ({ prompt, accessTok
- Clear Chat
+ {t("prompts.conversation.panel.clearChat")}
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/useConversation.ts b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/useConversation.ts
index 199c2cb3970..6fd351a3162 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/useConversation.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/conversation_panel/useConversation.ts
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from "react";
+import { useTranslation } from "react-i18next";
import { toast } from "@/lib/toast";
import { TokenUsage } from "@/components/chat_ui/ResponseMetrics";
import { Message } from "./types";
@@ -7,6 +8,7 @@ import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networ
import { streamErrorMessage } from "@/utils/streamErrorMessage";
export const useConversation = (prompt: any, accessToken: string | null) => {
+ const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState("");
@@ -38,12 +40,12 @@ export const useConversation = (prompt: any, accessToken: string | null) => {
const handleSendMessage = async () => {
if (!accessToken) {
- toast.fromError("Access token is required");
+ toast.fromError(t("prompts.conversation.toasts.tokenRequired"));
return;
}
if (extractedVariables.length > 0 && !allVariablesFilled) {
- toast.fromError("Please fill in all template variables");
+ toast.fromError(t("prompts.conversation.toasts.variablesRequired"));
return;
}
@@ -208,14 +210,14 @@ export const useConversation = (prompt: any, accessToken: string | null) => {
abortController.abort();
setAbortController(null);
setIsLoading(false);
- toast.info("Request cancelled");
+ toast.info(t("prompts.conversation.toasts.cancelled"));
}
};
const handleClearConversation = () => {
setMessages([]);
setVariablesFilled(false);
- toast.success("Chat history cleared.");
+ toast.success(t("prompts.conversation.toasts.cleared"));
};
const handleKeyDown = (event: React.KeyboardEvent) => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
index 2c2908a50f5..f7efb8175cc 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/index.tsx
@@ -1,4 +1,5 @@
import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
import ToolModal from "../tool_modal";
import { toast } from "@/lib/toast";
import { createPromptCall, updatePromptCall, getPromptInfo } from "@/components/networking";
@@ -15,13 +16,14 @@ import DotpromptViewTab from "./DotpromptViewTab";
import VersionHistorySidePanel from "./VersionHistorySidePanel";
const PromptEditorView: React.FC = ({ onClose, onSuccess, accessToken, initialPromptData }) => {
+ const { t } = useTranslation();
const getInitialPrompt = (): PromptType => {
if (initialPromptData) {
try {
return parseExistingPrompt(initialPromptData);
} catch (error) {
console.error("Error parsing existing prompt:", error);
- toast.fromError("Failed to parse prompt data");
+ toast.fromError(t("prompts.editor.toasts.parseFailed"));
}
}
return {
@@ -120,7 +122,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
try {
const parsed = JSON.parse(json);
const tool: Tool = {
- name: parsed.function?.name || "Unnamed Tool",
+ name: parsed.function?.name || t("prompts.editor.unnamedTool"),
description: parsed.function?.description || "",
json: json,
};
@@ -142,7 +144,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
setShowToolModal(false);
setEditingToolIndex(null);
} catch (error) {
- toast.fromError("Invalid JSON format");
+ toast.fromError(t("prompts.editor.toasts.invalidJson"));
}
};
@@ -171,7 +173,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
setActiveVersionId(`${versionData.prompt_id}.v${versionNum}`);
} catch (error) {
console.error("Error loading version:", error);
- toast.fromError("Failed to load prompt version");
+ toast.fromError(t("prompts.editor.toasts.loadVersionFailed"));
}
};
@@ -185,12 +187,12 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
const handleSave = async () => {
if (!accessToken) {
- toast.fromError("Access token is required");
+ toast.fromError(t("prompts.editor.toasts.tokenRequired"));
return;
}
if (!prompt.name || prompt.name.trim() === "") {
- toast.fromError("Please enter a valid prompt name");
+ toast.fromError(t("prompts.editor.toasts.nameRequired"));
return;
}
@@ -214,16 +216,16 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
if (editMode && initialPromptData?.prompt_spec?.prompt_id) {
await updatePromptCall(accessToken, initialPromptData.prompt_spec.prompt_id, promptData);
- toast.success("Prompt updated successfully!");
+ toast.success(t("prompts.editor.toasts.updated"));
} else {
await createPromptCall(accessToken, promptData);
- toast.success("Prompt created successfully!");
+ toast.success(t("prompts.editor.toasts.created"));
}
onSuccess();
onClose();
} catch (error) {
console.error("Error saving prompt:", error);
- toast.fromError(editMode ? "Failed to update prompt" : "Failed to save prompt");
+ toast.fromError(editMode ? t("prompts.editor.toasts.updateFailed") : t("prompts.editor.toasts.saveFailed"));
} finally {
setIsSaving(false);
setShowNameModal(false);
@@ -320,7 +322,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,
}`}
onClick={() => setViewMode("pretty")}
>
- PRETTY
+ {t("prompts.editor.viewModes.pretty")}
{
@@ -117,7 +118,7 @@ const parseToolsFromFrontmatter = (lines: string[]): Tool[] => {
try {
const toolObj = JSON.parse(rawJson);
tools.push({
- name: toolObj?.function?.name || "Unnamed Tool",
+ name: toolObj?.function?.name || i18n.t("prompts.editor.unnamedTool"),
description: toolObj?.function?.description || "",
json: JSON.stringify(toolObj, null, 2),
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
index 062e1d84a0a..9a37f026c12 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
+import { Trans, useTranslation } from "react-i18next";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -37,6 +38,7 @@ const PromptInfoView: React.FC = ({
onDelete,
onEdit,
}) => {
+ const { t } = useTranslation();
const [promptData, setPromptData] = useState(null);
const [promptTemplate, setPromptTemplate] = useState(null);
const [rawApiResponse, setRawApiResponse] = useState(null);
@@ -71,7 +73,7 @@ const PromptInfoView: React.FC = ({
}
setSelectedVersion(response.prompt_spec.version || null);
} catch (error) {
- toast.fromError("Failed to load prompt information");
+ toast.fromError(t("prompts.info.toasts.loadFailed"));
console.error("Error fetching prompt info:", error);
} finally {
setLoading(false);
@@ -118,11 +120,11 @@ const PromptInfoView: React.FC = ({
}, [selectedEnv]);
if (loading && !promptData) {
- return Loading...
;
+ return {t("prompts.info.loading")}
;
}
if (!promptData) {
- return Prompt not found
;
+ return {t("prompts.info.notFound")}
;
}
const formatDate = (dateString?: string) => {
@@ -150,12 +152,12 @@ const PromptInfoView: React.FC = ({
setIsDeleting(true);
try {
await deletePromptCall(accessToken, basePromptId);
- toast.success(`Prompt "${basePromptId}" deleted successfully`);
+ toast.success(t("prompts.info.toasts.deleted", { name: basePromptId }));
onDelete?.();
onClose();
} catch (error) {
console.error("Error deleting prompt:", error);
- toast.fromError("Failed to delete prompt");
+ toast.fromError(t("prompts.info.toasts.deleteFailed"));
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
@@ -178,7 +180,7 @@ const PromptInfoView: React.FC = ({
setPromptTemplate(response.raw_prompt_template);
setRawApiResponse(response);
} catch {
- toast.fromError(`Failed to load version v${versionNum}`);
+ toast.fromError(t("prompts.info.toasts.loadVersionFailed", { version: versionNum }));
}
};
@@ -193,11 +195,11 @@ const PromptInfoView: React.FC = ({
- Back to Prompts
+ {t("prompts.info.back")}
-
Prompt Details
+
{t("prompts.info.title")}
{basePromptId}
= ({
/>
onEdit?.(rawApiResponse)} className="flex items-center">
- Prompt Studio
+ {t("prompts.info.openStudio")}
{isAdmin && (
- Delete Prompt
+ {t("prompts.info.deletePrompt")}
)}
@@ -264,7 +266,9 @@ const PromptInfoView: React.FC
= ({
>
{env}
{versionHistory.length > 0 && selectedEnv === env && (
- (v{latestVersion})
+
+ {t("prompts.info.environmentVersion", { version: latestVersion })}
+
)}
))}
@@ -275,7 +279,7 @@ const PromptInfoView: React.FC = ({
{isViewingOldVersion && (
- Viewing v{selectedVersion} — not the latest version (v{latestVersion})
+ {t("prompts.info.oldVersionBanner", { selected: selectedVersion, latest: latestVersion })}
= ({
if (latest) handleVersionClick(latest);
}}
>
- Go to latest
+ {t("prompts.info.goToLatest")}
)}
@@ -293,15 +297,15 @@ const PromptInfoView: React.FC = ({
- Overview
+ {t("prompts.info.tabs.overview")}
{promptTemplate && (
- Prompt Template
+ {t("prompts.info.tabs.template")}
)}
- Raw JSON
+ {t("prompts.info.tabs.rawJson")}
@@ -310,51 +314,55 @@ const PromptInfoView: React.FC = ({
- Version
+ {t("prompts.info.overview.version")}
{currentVersion}
- v{currentVersion}
+ {t("prompts.info.versionLabel", { version: currentVersion })}
- Prompt Type
+ {t("prompts.info.overview.promptType")}
{promptData.prompt_info?.prompt_type || "-"}
- Created By
+ {t("prompts.info.overview.createdBy")}
{promptData.created_by || "-"}
- Created At
+ {t("prompts.info.overview.createdAt")}
{formatDate(promptData.created_at)}
-
Updated: {formatDate(promptData.updated_at)}
+
+ {t("prompts.info.overview.updated", { date: formatDate(promptData.updated_at) })}
+
{/* Version History Table */}
- Version History — {selectedEnv}
+
+ {t("prompts.info.history.title", { environment: selectedEnv })}
+
{loadingVersions ? (
- Loading versions...
+ {t("prompts.info.history.loading")}
) : versionHistory.length > 0 ? (
- Version
- Created By
- Date
- Actions
+ {t("prompts.info.history.columns.version")}
+ {t("prompts.info.history.columns.createdBy")}
+ {t("prompts.info.history.columns.date")}
+ {t("prompts.info.history.columns.actions")}
@@ -371,10 +379,12 @@ const PromptInfoView: React.FC = ({
onClick={() => handleVersionClick(v)}
>
- v{vNum}
+
+ {t("prompts.info.versionLabel", { version: vNum })}
+
{isLatest && (
- latest
+ {t("prompts.info.history.latest")}
)}
@@ -403,7 +413,7 @@ const PromptInfoView: React.FC = ({
}}
>
- Edit
+ {t("prompts.info.history.edit")}
@@ -412,7 +422,7 @@ const PromptInfoView: React.FC = ({
) : (
- No versions found in {selectedEnv}
+ {t("prompts.info.history.empty", { environment: selectedEnv })}
)}
@@ -422,7 +432,7 @@ const PromptInfoView: React.FC = ({
-
Prompt Template
+ {t("prompts.info.template.title")}
= ({
}`}
>
{copiedStates["prompt-content"] ? : }
- {copiedStates["prompt-content"] ? "Copied!" : "Copy Content"}
+ {copiedStates["prompt-content"] ? t("prompts.info.copied") : t("prompts.info.template.copy")}
-
Template ID
+
{t("prompts.info.template.id")}
{promptTemplate.litellm_prompt_id}
-
Content
+
{t("prompts.info.template.content")}
@@ -453,7 +463,7 @@ const PromptInfoView: React.FC
= ({
{promptTemplate.metadata && Object.keys(promptTemplate.metadata).length > 0 && (