From b03c393a54b97ae96dca09db42680d8b92c76894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B9=E9=9B=AA?= <2b@yorha.xyz> Date: Wed, 10 Jun 2026 13:51:24 +0900 Subject: [PATCH] feat(ui): i18n for pass-through settings, per-user usage, price reload, and prompts --- .../src/components/pass_through_settings.tsx | 52 ++++++----- .../src/components/per_user_usage.tsx | 46 +++++----- .../src/components/price_data_reload.tsx | 89 ++++++++++--------- .../src/components/prompts.tsx | 26 +++--- ui/litellm-dashboard/src/locales/en.json | 87 ++++++++++++++++++ ui/litellm-dashboard/src/locales/zh-CN.json | 87 ++++++++++++++++++ 6 files changed, 286 insertions(+), 101 deletions(-) diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx index 3d53de70727..6edbb870aad 100644 --- a/ui/litellm-dashboard/src/components/pass_through_settings.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -9,6 +9,7 @@ import { DataTable } from "./view_logs/table"; import { ColumnDef } from "@tanstack/react-table"; import { Eye, EyeOff } from "lucide-react"; import NotificationsManager from "./molecules/notifications_manager"; +import { useTranslation } from "react-i18next"; interface GeneralSettingsPageProps { accessToken: string | null; @@ -66,6 +67,7 @@ const PassThroughSettings: React.FC = ({ modelData, premiumUser, }) => { + const { t } = useTranslation(); const [generalSettings, setGeneralSettings] = useState([]); const [selectedEndpointId, setSelectedEndpointId] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -108,10 +110,10 @@ const PassThroughSettings: React.FC = ({ const updatedSettings = generalSettings.filter((setting) => setting.id !== endpointToDelete); setGeneralSettings(updatedSettings); - NotificationsManager.success("Endpoint deleted successfully."); + NotificationsManager.success(t("passThroughSettings.deleteSuccess")); } catch (error) { console.error("Error deleting the endpoint:", error); - NotificationsManager.fromBackend("Error deleting the endpoint: " + error); + NotificationsManager.fromBackend(t("passThroughSettings.deleteFailed", { error })); } // Close the confirmation modal and reset the endpointToDelete @@ -133,7 +135,7 @@ const PassThroughSettings: React.FC = ({ // Define columns for the DataTable const columns: ColumnDef[] = [ { - header: "ID", + header: t("passThroughSettings.colId"), accessorKey: "id", cell: (info: any) => ( @@ -147,19 +149,19 @@ const PassThroughSettings: React.FC = ({ ), }, { - header: "Path", + header: t("passThroughSettings.colPath"), accessorKey: "path", }, { - header: "Target", + header: t("passThroughSettings.colTarget"), accessorKey: "target", cell: (info: any) => {info.getValue()}, }, { header: () => (
- Methods - + {t("passThroughSettings.colMethods")} +
@@ -184,22 +186,24 @@ const PassThroughSettings: React.FC = ({ { header: () => (
- Authentication - + {t("passThroughSettings.colAuthentication")} +
), accessorKey: "auth", - cell: (info: any) => {info.getValue() ? "Yes" : "No"}, + cell: (info: any) => ( + {info.getValue() ? t("common.yes") : t("common.no")} + ), }, { - header: "Headers", + header: t("passThroughSettings.colHeaders"), accessorKey: "headers", cell: (info: any) => , }, { - header: "Actions", + header: t("common.actions"), id: "actions", cell: ({ row }) => (
@@ -207,13 +211,13 @@ const PassThroughSettings: React.FC = ({ icon={PencilAltIcon} size="sm" onClick={() => row.original.id && setSelectedEndpointId(row.original.id)} - title="Edit" + title={t("common.edit")} /> handleResetField(row.original.id!, row.index)} - title="Delete" + title={t("common.delete")} />
), @@ -232,7 +236,7 @@ const PassThroughSettings: React.FC = ({ const selectedEndpoint = generalSettings.find((endpoint) => endpoint.id === selectedEndpointId); if (!selectedEndpoint) { - return
Endpoint not found
; + return
{t("passThroughSettings.endpointNotFound")}
; } return ( @@ -250,8 +254,8 @@ const PassThroughSettings: React.FC = ({ return (
- Pass Through Endpoints - Configure and manage your pass-through endpoints + {t("passThroughSettings.title")} + {t("passThroughSettings.subtitle")}
= ({ renderSubComponent={() =>
} getRowCanExpand={() => false} isLoading={false} - noDataMessage="No pass-through endpoints configured" + noDataMessage={t("passThroughSettings.noData")} /> {isDeleteModalOpen && ( @@ -287,20 +291,20 @@ const PassThroughSettings: React.FC = ({
-

Delete Pass-Through Endpoint

+

+ {t("passThroughSettings.deleteModalTitle")} +

-

- Are you sure you want to delete this pass-through endpoint? This action cannot be undone. -

+

{t("passThroughSettings.deleteModalMessage")}

- +
diff --git a/ui/litellm-dashboard/src/components/per_user_usage.tsx b/ui/litellm-dashboard/src/components/per_user_usage.tsx index 59aed14292e..c32b27eb737 100644 --- a/ui/litellm-dashboard/src/components/per_user_usage.tsx +++ b/ui/litellm-dashboard/src/components/per_user_usage.tsx @@ -18,6 +18,7 @@ import { TabPanels, } from "@tremor/react"; import { perUserAnalyticsCall } from "./networking"; +import { useTranslation } from "react-i18next"; interface PerUserMetrics { user_id: string; @@ -45,6 +46,7 @@ interface PerUserUsageProps { } const PerUserUsage: React.FC = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => { + const { t } = useTranslation(); // Maximum number of user agent categories to show in charts to prevent color palette overflow const MAX_USER_AGENTS = 8; const [perUserData, setPerUserData] = useState({ @@ -95,13 +97,13 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, return (
- Per User Usage - Individual developer usage metrics + {t("perUserUsage.title")} + {t("perUserUsage.subtitle")} - User Details - Usage Distribution + {t("perUserUsage.tabUserDetails")} + {t("perUserUsage.tabUsageDistribution")} @@ -110,13 +112,13 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, - User ID - User Email - User Agent - Success Generations - Total Tokens - Failed Requests - Total Cost + {t("perUserUsage.colUserId")} + {t("perUserUsage.colUserEmail")} + {t("perUserUsage.colUserAgent")} + {t("perUserUsage.colSuccessGenerations")} + {t("perUserUsage.colTotalTokens")} + {t("perUserUsage.colFailedRequests")} + {t("perUserUsage.colTotalCost")} @@ -126,10 +128,10 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {item.user_id} - {item.user_email || "N/A"} + {item.user_email || t("perUserUsage.naValue")} - {item.user_agent || "Unknown"} + {item.user_agent || t("common.unknown")} {formatAbbreviatedNumber(item.successful_requests)} @@ -150,10 +152,12 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {perUserData.results.length > 10 && (
- Showing 10 of {perUserData.total_count} results + + {t("perUserUsage.showing10Of", { total: perUserData.total_count })} +
@@ -171,8 +175,8 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {/* Tab 2: Usage Distribution Histogram */}
- User Usage Distribution - Number of users by successful request frequency + {t("perUserUsage.distributionTitle")} + {t("perUserUsage.distributionSubtitle")}
= ({ accessToken, selectedTags, // Get top user agents by frequency first const userAgentCounts = new Map(); perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + const agent = item.user_agent || t("common.unknown"); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); @@ -234,7 +238,7 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, // Count user agents by frequency and get top ones const userAgentCounts = new Map(); perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + const agent = item.user_agent || t("common.unknown"); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); @@ -245,7 +249,7 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, .map(([agent]) => agent); })()} colors={["blue", "green", "orange", "red", "purple", "yellow", "pink", "indigo"]} - valueFormatter={(value: number) => `${value} users`} + valueFormatter={(value: number) => t("perUserUsage.usersCount", { count: value })} yAxisWidth={80} showLegend={true} stack={true} diff --git a/ui/litellm-dashboard/src/components/price_data_reload.tsx b/ui/litellm-dashboard/src/components/price_data_reload.tsx index 05a29ba3fcb..96e04debff1 100644 --- a/ui/litellm-dashboard/src/components/price_data_reload.tsx +++ b/ui/litellm-dashboard/src/components/price_data_reload.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider } from "antd"; import { ReloadOutlined, @@ -48,12 +49,14 @@ interface PriceDataReloadProps { const PriceDataReload: React.FC = ({ accessToken, onReloadSuccess, - buttonText = "Reload Price Data", + buttonText, showIcon = true, size = "middle", type = "primary", className = "", }) => { + const { t } = useTranslation(); + const resolvedButtonText = buttonText ?? t("priceDataReload.reloadButton"); const [isLoading, setIsLoading] = useState(false); const [isScheduling, setIsScheduling] = useState(false); const [isCancelling, setIsCancelling] = useState(false); @@ -117,7 +120,7 @@ const PriceDataReload: React.FC = ({ const handleHardRefresh = async () => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + NotificationsManager.fromBackend(t("priceDataReload.noAccessToken")); return; } @@ -126,29 +129,29 @@ const PriceDataReload: React.FC = ({ const response = await reloadModelCostMap(accessToken); if (response.status === "success") { - NotificationsManager.success(`Price data reloaded successfully! ${response.models_count || 0} models updated.`); + NotificationsManager.success(t("priceDataReload.reloadSuccess", { count: response.models_count || 0 })); onReloadSuccess?.(); // Refresh status and source info after successful reload await fetchReloadStatus(); await fetchSourceInfo(); } else { - NotificationsManager.fromBackend("Failed to reload price data"); + NotificationsManager.fromBackend(t("priceDataReload.reloadFailed")); } } catch (error) { console.error("Error reloading price data:", error); - NotificationsManager.fromBackend("Failed to reload price data. Please try again."); + NotificationsManager.fromBackend(t("priceDataReload.reloadFailedRetry")); } finally { setIsLoading(false); } }; const handleScheduleReload = async () => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + NotificationsManager.fromBackend(t("priceDataReload.noAccessToken")); return; } if (hours <= 0) { - NotificationsManager.fromBackend("Hours must be greater than 0"); + NotificationsManager.fromBackend(t("priceDataReload.hoursMustBePositive")); return; } @@ -157,15 +160,15 @@ const PriceDataReload: React.FC = ({ const response = await scheduleModelCostMapReload(accessToken, hours); if (response.status === "success") { - NotificationsManager.success(`Periodic reload scheduled for every ${hours} hours`); + NotificationsManager.success(t("priceDataReload.scheduleSuccess", { hours })); setShowScheduleModal(false); await fetchReloadStatus(); } else { - NotificationsManager.fromBackend("Failed to schedule periodic reload"); + NotificationsManager.fromBackend(t("priceDataReload.scheduleFailed")); } } catch (error) { console.error("Error scheduling reload:", error); - NotificationsManager.fromBackend("Failed to schedule periodic reload. Please try again."); + NotificationsManager.fromBackend(t("priceDataReload.scheduleFailedRetry")); } finally { setIsScheduling(false); } @@ -173,7 +176,7 @@ const PriceDataReload: React.FC = ({ const handleCancelReload = async () => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + NotificationsManager.fromBackend(t("priceDataReload.noAccessToken")); return; } @@ -182,21 +185,21 @@ const PriceDataReload: React.FC = ({ const response = await cancelModelCostMapReload(accessToken); if (response.status === "success") { - NotificationsManager.success("Periodic reload cancelled successfully"); + NotificationsManager.success(t("priceDataReload.cancelSuccess")); await fetchReloadStatus(); } else { - NotificationsManager.fromBackend("Failed to cancel periodic reload"); + NotificationsManager.fromBackend(t("priceDataReload.cancelFailed")); } } catch (error) { console.error("Error cancelling reload:", error); - NotificationsManager.fromBackend("Failed to cancel periodic reload. Please try again."); + NotificationsManager.fromBackend(t("priceDataReload.cancelFailedRetry")); } finally { setIsCancelling(false); } }; const formatDateTime = (dateTimeString: string | null) => { - if (!dateTimeString) return "Never"; + if (!dateTimeString) return t("common.never"); try { return new Date(dateTimeString).toLocaleString(); } catch { @@ -205,9 +208,9 @@ const PriceDataReload: React.FC = ({ }; const getStatusText = () => { - if (!reloadStatus?.scheduled) return "Not scheduled"; - if (!reloadStatus.last_run) return "Ready"; - return "Active"; + if (!reloadStatus?.scheduled) return t("priceDataReload.statusNotScheduled"); + if (!reloadStatus.last_run) return t("priceDataReload.statusReady"); + return t("priceDataReload.statusActive"); }; const getStatusColor = () => { @@ -222,11 +225,11 @@ const PriceDataReload: React.FC = ({ {/* Hard Refresh Button - Always visible */} = ({ e.currentTarget.style.backgroundColor = "#6366f1"; }} > - {buttonText} + {resolvedButtonText} @@ -294,7 +297,7 @@ const PriceDataReload: React.FC = ({ lineHeight: "1.25rem", }} > - Set Up Periodic Reload + {t("priceDataReload.setupPeriodicReload")} ) : ( )} @@ -340,13 +343,13 @@ const PriceDataReload: React.FC = ({ )} - Pricing Data Source + {t("priceDataReload.pricingDataSource")} - {sourceInfo.source === "remote" ? "Remote" : "Local"} + {sourceInfo.source === "remote" ? t("priceDataReload.sourceRemote") : t("priceDataReload.sourceLocal")} @@ -355,7 +358,7 @@ const PriceDataReload: React.FC = ({ {/* Model count */}
- Models loaded: + {t("priceDataReload.modelsLoaded")} {sourceInfo.model_count.toLocaleString()} @@ -366,7 +369,7 @@ const PriceDataReload: React.FC = ({ {sourceInfo.url && (
- {sourceInfo.source === "remote" ? "Loaded from:" : "Attempted URL:"} + {sourceInfo.source === "remote" ? t("priceDataReload.loadedFrom") : t("priceDataReload.attemptedUrl")} = ({
- Local mode forced via LITELLM_LOCAL_MODEL_COST_MAP=True + {t("priceDataReload.localModeForced")}
)} @@ -413,7 +416,7 @@ const PriceDataReload: React.FC = ({ > - Fell back to local: {sourceInfo.fallback_reason} + {t("priceDataReload.fellBackToLocal", { reason: sourceInfo.fallback_reason })}
)} @@ -435,16 +438,16 @@ const PriceDataReload: React.FC = ({ {reloadStatus.scheduled ? (
}> - Scheduled every {reloadStatus.interval_hours} hours + {t("priceDataReload.scheduledEvery", { hours: reloadStatus.interval_hours })}
) : ( - No periodic reload scheduled + {t("priceDataReload.noPeriodicReload")} )}
- Last run: + {t("priceDataReload.lastRun")} {formatDateTime(reloadStatus.last_run)}
@@ -454,14 +457,14 @@ const PriceDataReload: React.FC = ({ {reloadStatus.next_run && (
- Next run: + {t("priceDataReload.nextRun")} {formatDateTime(reloadStatus.next_run)}
)}
- Status: + {t("common.status")} {getStatusText()}
@@ -473,13 +476,13 @@ const PriceDataReload: React.FC = ({ {/* Schedule Modal */} setShowScheduleModal(false)} confirmLoading={isScheduling} - okText="Schedule" - cancelText="Cancel" + okText={t("priceDataReload.scheduleButton")} + cancelText={t("common.cancel")} okButtonProps={{ style: { backgroundColor: "#6366f1", @@ -489,7 +492,7 @@ const PriceDataReload: React.FC = ({ }} >
- Set up automatic reload of price data every: + {t("priceDataReload.scheduleModalDesc")}
= ({ max={168} // 1 week max value={hours} onChange={(value) => setHours(value || 6)} - addonAfter="hours" + addonAfter={t("priceDataReload.hoursLabel")} style={{ width: "100%" }} />
- - This will automatically fetch the latest pricing data from the remote source every {hours} hours. - + {t("priceDataReload.scheduleModalHint", { hours })}
diff --git a/ui/litellm-dashboard/src/components/prompts.tsx b/ui/litellm-dashboard/src/components/prompts.tsx index 1e0155a7738..440d240ba65 100644 --- a/ui/litellm-dashboard/src/components/prompts.tsx +++ b/ui/litellm-dashboard/src/components/prompts.tsx @@ -9,6 +9,7 @@ import AddPromptForm from "./prompts/add_prompt_form"; import PromptEditorView from "./prompts/prompt_editor_view"; import NotificationsManager from "./molecules/notifications_manager"; import { isAdminRole, isProxyAdminRole } from "@/utils/roles"; +import { useTranslation } from "react-i18next"; interface PromptsProps { accessToken: string | null; @@ -16,6 +17,7 @@ interface PromptsProps { } const PromptsPanel: React.FC = ({ accessToken, userRole }) => { + const { t } = useTranslation(); const [promptsList, setPromptsList] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedEnvironment, setSelectedEnvironment] = useState(undefined); @@ -101,11 +103,11 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { setIsDeleting(true); try { await deletePromptCall(accessToken, promptToDelete.id); - NotificationsManager.success(`Prompt "${promptToDelete.name}" deleted successfully`); + NotificationsManager.success(t("prompts.deleteSuccess", { name: promptToDelete.name })); fetchPrompts(); // Refresh the list } catch (error) { console.error("Error deleting prompt:", error); - NotificationsManager.fromBackend("Failed to delete prompt"); + NotificationsManager.fromBackend(t("prompts.deleteFailed")); } finally { setIsDeleting(false); setPromptToDelete(null); @@ -141,24 +143,24 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { {canModify && ( <> )}