diff --git a/ui/litellm-dashboard/src/components/SCIM.tsx b/ui/litellm-dashboard/src/components/SCIM.tsx index c8da8f2c9c7..a0fdcbeb5cf 100644 --- a/ui/litellm-dashboard/src/components/SCIM.tsx +++ b/ui/litellm-dashboard/src/components/SCIM.tsx @@ -12,6 +12,7 @@ import { } from "@ant-design/icons"; import { parseErrorMessage } from "./shared/errorUtils"; import NotificationsManager from "./molecules/notifications_manager"; +import { useTranslation } from "react-i18next"; interface SCIMConfigProps { accessToken: string | null; @@ -20,6 +21,7 @@ interface SCIMConfigProps { } const SCIMConfig: React.FC = ({ accessToken, userID, proxySettings }) => { + const { t } = useTranslation(); const [form] = Form.useForm(); const [isCreatingToken, setIsCreatingToken] = useState(false); const [tokenData, setTokenData] = useState(null); @@ -42,7 +44,7 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti const handleCreateSCIMToken = async (values: any) => { if (!accessToken || !userID) { - NotificationsManager.fromBackend("You need to be logged in to create a SCIM token"); + NotificationsManager.fromBackend(t("scim.needToBeLoggedIn")); return; } @@ -58,10 +60,10 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti const response = await keyCreateCall(accessToken, userID, formData); setTokenData(response); - NotificationsManager.success("SCIM token created successfully"); + NotificationsManager.success(t("scim.createTokenSuccess")); } catch (error: any) { console.error("Error creating SCIM token:", error); - NotificationsManager.fromBackend("Failed to create SCIM token: " + parseErrorMessage(error)); + NotificationsManager.fromBackend(t("scim.createTokenFailed", { error: parseErrorMessage(error) })); } finally { setIsCreatingToken(false); } @@ -71,12 +73,9 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti
- SCIM Configuration + {t("scim.title")}
- - System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and - groups in LiteLLM. - + {t("scim.description")} @@ -89,21 +88,16 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti <LinkOutlined className="h-5 w-5 mr-2" /> - SCIM Tenant URL + {t("scim.tenantUrlTitle")} - - Use this URL in your identity provider SCIM integration settings. - + {t("scim.tenantUrlDesc")}
- NotificationsManager.success("URL copied to clipboard")} - > + NotificationsManager.success(t("scim.urlCopied"))}> - Copy + {t("common.copy")}
@@ -117,13 +111,12 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti <KeyOutlined className="h-5 w-5 mr-2" /> - Authentication Token + {t("scim.authTokenTitle")} - - You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider - configuration. + + {t("scim.calloutDesc")} {!tokenData ? ( @@ -131,10 +124,10 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti
- + = ({ accessToken, userID, proxySetti className="flex items-center" > - Create SCIM Token + {t("scim.createTokenButton")}
@@ -153,11 +146,9 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti
- Your SCIM Token + {t("scim.yourScimTokenTitle")}
- - Make sure to copy this token now. You will not be able to see it again. - + {t("scim.tokenWarning")}
= ({ accessToken, userID, proxySetti /> NotificationsManager.success("Token copied to clipboard")} + onCopy={() => NotificationsManager.success(t("scim.tokenCopied"))} > - Copy + {t("common.copy")}
setTokenData(null)}> - Create Another Token + {t("scim.createAnotherToken")}
)} diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index e063b0ed431..066d1cc4839 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -4,6 +4,7 @@ import { Text, TextInput } from "@tremor/react"; import { getSSOSettings, updateSSOSettings } from "./networking"; import NotificationsManager from "./molecules/notifications_manager"; import { parseErrorMessage } from "./shared/errorUtils"; +import { useTranslation } from "react-i18next"; interface SSOModalsProps { isAddSSOModalVisible: boolean; @@ -101,6 +102,19 @@ const ssoProviderConfigs: Record = { }, }; +const fieldLabelKeys: Record = { + google_client_id: "ssoModals.fieldLabels.googleClientId", + google_client_secret: "ssoModals.fieldLabels.googleClientSecret", + microsoft_client_id: "ssoModals.fieldLabels.microsoftClientId", + microsoft_client_secret: "ssoModals.fieldLabels.microsoftClientSecret", + microsoft_tenant: "ssoModals.fieldLabels.microsoftTenant", + generic_client_id: "ssoModals.fieldLabels.genericClientId", + generic_client_secret: "ssoModals.fieldLabels.genericClientSecret", + generic_authorization_endpoint: "ssoModals.fieldLabels.authorizationEndpoint", + generic_token_endpoint: "ssoModals.fieldLabels.tokenEndpoint", + generic_userinfo_endpoint: "ssoModals.fieldLabels.userinfoEndpoint", +}; + const SSOModals: React.FC = ({ isAddSSOModalVisible, isInstructionsModalVisible, @@ -113,6 +127,7 @@ const SSOModals: React.FC = ({ accessToken, ssoConfigured = false, // Default to false if not provided }) => { + const { t } = useTranslation(); const [isClearConfirmModalVisible, setIsClearConfirmModalVisible] = useState(false); // Load existing SSO settings when modal opens @@ -196,7 +211,7 @@ const SSOModals: React.FC = ({ // Enhanced form submission handler const handleFormSubmit = async (formValues: Record) => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + NotificationsManager.fromBackend(t("ssoModals.noAccessToken")); return; } @@ -254,14 +269,14 @@ const SSOModals: React.FC = ({ // Continue with the original flow (show instructions) handleShowInstructions(formValues); } catch (error: unknown) { - NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error)); + NotificationsManager.fromBackend(t("ssoModals.saveSettingsFailed", { error: parseErrorMessage(error) })); } }; // Handle clearing SSO settings const handleClearSSO = async () => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + NotificationsManager.fromBackend(t("ssoModals.noAccessToken")); return; } @@ -295,10 +310,10 @@ const SSOModals: React.FC = ({ // Close the main SSO modal and trigger refresh handleAddSSOOk(); - NotificationsManager.success("SSO settings cleared successfully"); + NotificationsManager.success(t("ssoModals.clearSettingsSuccess")); } catch (error) { console.error("Failed to clear SSO settings:", error); - NotificationsManager.fromBackend("Failed to clear SSO settings"); + NotificationsManager.fromBackend(t("ssoModals.clearSettingsFailed")); } }; @@ -307,22 +322,26 @@ const SSOModals: React.FC = ({ const config = ssoProviderConfigs[provider]; if (!config) return null; - return config.fields.map((field) => ( - - {field.name.includes("client") ? : } - - )); + return config.fields.map((field) => { + const labelKey = fieldLabelKeys[field.name]; + const label = labelKey ? t(labelKey) : field.label; + return ( + + {field.name.includes("client") ? : } + + ); + }); }; return ( <> = ({ > <> - Internal Viewer - Internal User - Admin Viewer - Proxy Admin + {t("ssoModals.roleInternalViewer")} + {t("ssoModals.roleInternalUser")} + {t("ssoModals.roleAdminViewer")} + {t("ssoModals.roleProxyAdmin")} - + - + - + - + @@ -506,22 +529,22 @@ const SSOModals: React.FC = ({ e.currentTarget.style.borderColor = "#6366f1"; }} > - Clear + {t("common.clear")} )} - Save + {t("common.save")} {/* Clear Confirmation Modal */} setIsClearConfirmModalVisible(false)} - okText="Yes, Clear" - cancelText="Cancel" + okText={t("ssoModals.clearConfirmOk")} + cancelText={t("common.cancel")} okButtonProps={{ danger: true, style: { @@ -530,25 +553,25 @@ const SSOModals: React.FC = ({ }, }} > -

Are you sure you want to clear all SSO settings? This action cannot be undone.

-

Users will no longer be able to login using SSO after this change.

+

{t("ssoModals.clearConfirmBody1")}

+

{t("ssoModals.clearConfirmBody2")}

-

Follow these steps to complete the SSO setup:

- 1. DO NOT Exit this TAB - 2. Open a new tab, visit your proxy base url - 3. Confirm your SSO is configured correctly and you can login on the new Tab - 4. If Step 3 is successful, you can close this tab +

{t("ssoModals.instructionsIntro")}

+ {t("ssoModals.instructionStep1")} + {t("ssoModals.instructionStep2")} + {t("ssoModals.instructionStep3")} + {t("ssoModals.instructionStep4")}
- Done + {t("common.done")}
diff --git a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx index 9771f4b6c8f..fb4f73b2bf2 100644 --- a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx +++ b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx @@ -6,6 +6,7 @@ import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_compone import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; import NotificationsManager from "./molecules/notifications_manager"; import { ModelSelect } from "./ModelSelect/ModelSelect"; +import { useTranslation } from "react-i18next"; const { Title, Text } = Typography; @@ -52,7 +53,10 @@ const SettingRow: React.FC = ({ label, description, isEditing, ); -const NotSet = () => Not set; +const NotSet = () => { + const { t } = useTranslation(); + return {t("teamSsoSettings.notSet")}; +}; const renderTags = (values: string[], displayFn?: (v: string) => string) => { if (!values || values.length === 0) return ; @@ -86,6 +90,7 @@ const DEFAULT_VALUES: SettingsValues = { }; const TeamSSOSettings: React.FC = ({ accessToken }) => { + const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [values, setValues] = useState(DEFAULT_VALUES); const [isEditing, setIsEditing] = useState(false); @@ -108,7 +113,7 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { } catch (error) { console.error("Error fetching team SSO settings:", error); setFetchError(true); - NotificationsManager.fromBackend("Failed to fetch team settings"); + NotificationsManager.fromBackend(t("teamSsoSettings.fetchFailed")); } finally { setLoading(false); } @@ -127,10 +132,10 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { setValues(newValues); setEditedValues(newValues); setIsEditing(false); - NotificationsManager.success("Default team settings updated successfully"); + NotificationsManager.success(t("teamSsoSettings.saveSuccess")); } catch (error) { console.error("Error updating team settings:", error); - NotificationsManager.fromBackend("Failed to update team settings"); + NotificationsManager.fromBackend(t("teamSsoSettings.saveFailed")); } finally { setSaving(false); } @@ -156,7 +161,7 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { if (fetchError) { return ( - No team settings available or you do not have permission to view them. + {t("teamSsoSettings.noSettings")} ); } @@ -167,25 +172,23 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => {
- Default Team Settings + {t("teamSsoSettings.title")} - - These settings will be applied by default when creating new teams. - + {t("teamSsoSettings.subtitle")}
{isEditing ? (
) : ( )}
@@ -194,11 +197,13 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => {
{/* Budget & Rate Limits */}
-
Budget & Rate Limits
+
+ {t("teamSsoSettings.sectionBudgetRateLimits")} +
${Number(values.max_budget).toLocaleString()} : @@ -209,7 +214,7 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { style={{ maxWidth: 320 }} value={editedValues.max_budget} onChange={(v) => update("max_budget", v)} - placeholder="Not set" + placeholder={t("teamSsoSettings.notSetPlaceholder")} prefix="$" min={0} /> @@ -217,8 +222,8 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { /> {getBudgetDurationLabel(values.budget_duration)} : @@ -233,8 +238,8 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { /> {values.tpm_limit.toLocaleString()} : } editContent={ @@ -243,15 +248,15 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { style={{ maxWidth: 320 }} value={editedValues.tpm_limit} onChange={(v) => update("tpm_limit", v)} - placeholder="Not set" + placeholder={t("teamSsoSettings.notSetPlaceholder")} min={0} /> } /> {values.rpm_limit.toLocaleString()} : } editContent={ @@ -260,7 +265,7 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { style={{ maxWidth: 320 }} value={editedValues.rpm_limit} onChange={(v) => update("rpm_limit", v)} - placeholder="Not set" + placeholder={t("teamSsoSettings.notSetPlaceholder")} min={0} /> } @@ -270,11 +275,13 @@ const TeamSSOSettings: React.FC = ({ accessToken }) => { {/* Access & Permissions */}
-
Access & Permissions
+
+ {t("teamSsoSettings.sectionAccessPermissions")} +
= ({ accessToken }) => { /> = ({ accessToken }) => { style={{ width: "100%" }} value={editedValues.team_member_permissions || []} onChange={(v) => update("team_member_permissions", v)} - placeholder="Select permissions" + placeholder={t("teamSsoSettings.selectPermissionsPlaceholder")} tagRender={({ label, closable, onClose }) => ( {label} diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx index b0fa6884dd3..1800b4127bf 100644 --- a/ui/litellm-dashboard/src/components/ToolDetail.tsx +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -20,6 +20,7 @@ import { type ToolPolicyOverrideRow, } from "@/components/networking"; import type { Team } from "@/components/key_team_helpers/key_list"; +import { useTranslation } from "react-i18next"; interface ToolDetailProps { toolName: string; @@ -45,6 +46,7 @@ function getDefaultLogsDateRange(): { start: string; end: string } { } export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { + const { t } = useTranslation(); const queryClient = useQueryClient(); const [overrideSaving, setOverrideSaving] = useState(false); const [inputPolicySaving, setInputPolicySaving] = useState(false); @@ -145,12 +147,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); invalidateDetail(); } catch (e: unknown) { - alert(`Failed to update input policy: ${e instanceof Error ? e.message : String(e)}`); + alert(t("toolDetail.updateInputPolicyFailed", { error: e instanceof Error ? e.message : String(e) })); } finally { setInputPolicySaving(false); } }, - [accessToken, toolName, invalidateDetail], + [accessToken, toolName, invalidateDetail, t], ); const handleOutputPolicyChange = useCallback( @@ -161,12 +163,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); invalidateDetail(); } catch (e: unknown) { - alert(`Failed to update output policy: ${e instanceof Error ? e.message : String(e)}`); + alert(t("toolDetail.updateOutputPolicyFailed", { error: e instanceof Error ? e.message : String(e) })); } finally { setOutputPolicySaving(false); } }, - [accessToken, toolName, invalidateDetail], + [accessToken, toolName, invalidateDetail, t], ); const handleAddOverride = useCallback(async () => { @@ -190,11 +192,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { setBlockTeamId(null); setBlockKey(null); } catch (e: unknown) { - alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`); + alert(t("toolDetail.addOverrideFailed", { error: e instanceof Error ? e.message : String(e) })); } finally { setOverrideSaving(false); } - }, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]); + }, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail, t]); const handleRemoveOverride = useCallback( async (override: ToolPolicyOverrideRow) => { @@ -207,12 +209,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { }); invalidateDetail(); } catch (e: unknown) { - alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`); + alert(t("toolDetail.removeOverrideFailed", { error: e instanceof Error ? e.message : String(e) })); } finally { setOverrideSaving(false); } }, - [accessToken, toolName, invalidateDetail], + [accessToken, toolName, invalidateDetail, t], ); if (detailLoading && !detail) { @@ -227,9 +229,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { return (
-

Failed to load tool details.

+

{t("toolDetail.failedToLoad")}

); } @@ -247,7 +249,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
@@ -259,13 +261,13 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { {tool.origin ?? "—"} - {(tool.call_count ?? 0).toLocaleString()} calls + {t("toolDetail.callCount", { count: tool.call_count ?? 0 })}
{tool.user_agent && (
-
User Agent:
+
{t("toolDetail.userAgentLabel")}
{tool.user_agent}
@@ -273,13 +275,15 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { )} {tool.created_at && (
-
First Discovered:
+
+ {t("toolDetail.firstDiscoveredLabel")} +
{new Date(tool.created_at).toLocaleString()}
)} {tool.last_used_at && (
-
Last Used:
+
{t("toolDetail.lastUsedLabel")}
{new Date(tool.last_used_at).toLocaleString()}
)} @@ -292,10 +296,8 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { {/* Two-panel policy layout */}
-

Input Policy

-

- {inputDesc ?? "Controls what data this tool is allowed to accept."} -

+

{t("toolDetail.inputPolicyTitle")}

+

{inputDesc ?? t("toolDetail.inputPolicyDefaultDesc")}

-

Output Policy

-

- {outputDesc ?? "Controls how this tool's output is trusted by downstream tools."} -

+

{t("toolDetail.outputPolicyTitle")}

+

{outputDesc ?? t("toolDetail.outputPolicyDefaultDesc")}

0 && (
-

Blocked for team or key

+

{t("toolDetail.blockedForTitle")}

    {overrides.map((ov) => (
  • - {ov.team_id ? `Team: ${ov.team_id}` : ""} + {ov.team_id ? t("toolDetail.teamLabel", { id: ov.team_id }) : ""} {ov.team_id && ov.key_hash ? " · " : ""} - {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} + {ov.key_hash ? t("toolDetail.keyLabel", { id: ov.key_alias || ov.key_hash.substring(0, 8) }) : ""} {!ov.team_id && !ov.key_hash ? "—" : ""}
  • ))} @@ -354,10 +354,10 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { )}
    -

    Block for team or key

    +

    {t("toolDetail.blockForTitle")}

    - Scope + {t("toolDetail.scopeLabel")}
    - {blockScope === "team" ? "Team" : "Key"} + {blockScope === "team" ? t("toolDetail.scopeTeam") : t("toolDetail.scopeKey")} {blockScope === "team" ? ( setBlockTeamId(id || null)} /> ) : (