diff --git a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx index 4c182bdfae4..4105433ae09 100644 --- a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx +++ b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx @@ -14,6 +14,7 @@ import Papa from "papaparse"; import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline"; import { CopyToClipboard } from "react-copy-to-clipboard"; import NotificationsManager from "./molecules/notifications_manager"; +import { useTranslation } from "react-i18next"; interface BulkCreateUsersProps { accessToken: string; @@ -52,6 +53,7 @@ const BulkCreateUsersButton: React.FC = ({ possibleUIRoles, onUsersCreated, }) => { + const { t } = useTranslation(); const [isModalVisible, setIsModalVisible] = useState(false); const [parsedData, setParsedData] = useState([]); const [isProcessing, setIsProcessing] = useState(false); @@ -109,16 +111,14 @@ const BulkCreateUsersButton: React.FC = ({ // Check file type if (file.type !== "text/csv" && !file.name.endsWith(".csv")) { - setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`); - NotificationsManager.fromBackend("Invalid file type. Please upload a CSV file."); + setFileError(t("bulkCreateUsers.invalidFileType", { name: file.name })); + NotificationsManager.fromBackend(t("bulkCreateUsers.invalidFileTypeShort")); return false; } // Check file size (limit to 5MB) if (file.size > 5 * 1024 * 1024) { - setFileError( - `File is too large (${(file.size / (1024 * 1024)).toFixed(1)} MB). Please upload a CSV file smaller than 5MB.`, - ); + setFileError(t("bulkCreateUsers.fileTooLarge", { size: (file.size / (1024 * 1024)).toFixed(1) })); return false; } @@ -126,16 +126,14 @@ const BulkCreateUsersButton: React.FC = ({ complete: (results) => { // Check if file is empty if (!results.data || results.data.length === 0) { - setCsvStructureError("The CSV file appears to be empty. Please upload a file with data."); + setCsvStructureError(t("bulkCreateUsers.csvEmpty")); setParsedData([]); return; } // Check if there's only header row if (results.data.length === 1) { - setCsvStructureError( - "The CSV file only contains headers but no user data. Please add user data to your CSV.", - ); + setCsvStructureError(t("bulkCreateUsers.csvHeaderOnly")); setParsedData([]); return; } @@ -144,9 +142,7 @@ const BulkCreateUsersButton: React.FC = ({ // Check if headers exist if (headers.length === 0 || (headers.length === 1 && headers[0] === "")) { - setCsvStructureError( - "The CSV file doesn't contain any column headers. Please make sure your CSV has headers.", - ); + setCsvStructureError(t("bulkCreateUsers.csvNoHeaders")); setParsedData([]); return; } @@ -156,9 +152,7 @@ const BulkCreateUsersButton: React.FC = ({ // Check if all required columns are present const missingColumns = requiredColumns.filter((col) => !headers.includes(col)); if (missingColumns.length > 0) { - setCsvStructureError( - `Your CSV is missing these required columns: ${missingColumns.join(", ")}. Please add these columns to your CSV file.`, - ); + setCsvStructureError(t("bulkCreateUsers.csvMissingColumns", { columns: missingColumns.join(", ") })); setParsedData([]); return; } @@ -177,7 +171,7 @@ const BulkCreateUsersButton: React.FC = ({ return { rowNumber: index + 2, isValid: false, - error: `Row ${index + 2} has fewer columns than the header row. Please ensure all data is properly formatted.`, + error: t("bulkCreateUsers.rowTooFewColumns", { row: index + 2 }), user_email: "", user_role: "", } as UserData; @@ -200,36 +194,36 @@ const BulkCreateUsersButton: React.FC = ({ // Email validation if (!user.user_email) { - errors.push("Email is required"); + errors.push(t("bulkCreateUsers.emailRequired")); } else if (!user.user_email.includes("@") || !user.user_email.includes(".")) { - errors.push("Invalid email format (must contain @ and domain)"); + errors.push(t("bulkCreateUsers.emailInvalid")); } // Role validation if (!user.user_role) { - errors.push("Role is required"); + errors.push(t("bulkCreateUsers.roleRequired")); } else { // Validate user role const validRoles = ["proxy_admin", "proxy_admin_viewer", "internal_user", "internal_user_viewer"]; if (!validRoles.includes(user.user_role)) { - errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(", ")}`); + errors.push( + t("bulkCreateUsers.roleInvalid", { role: user.user_role, validRoles: validRoles.join(", ") }), + ); } } // Budget validation if (user.max_budget && user.max_budget.toString().trim() !== "") { if (isNaN(parseFloat(user.max_budget.toString()))) { - errors.push(`Max budget "${user.max_budget}" must be a number`); + errors.push(t("bulkCreateUsers.budgetNotNumber", { budget: user.max_budget })); } else if (parseFloat(user.max_budget.toString()) <= 0) { - errors.push("Max budget must be greater than 0"); + errors.push(t("bulkCreateUsers.budgetMustBePositive")); } } // Budget duration validation if (user.budget_duration && !user.budget_duration.match(/^\d+[dhmwy]$|^\d+mo$/)) { - errors.push( - `Invalid budget duration format "${user.budget_duration}". Use format like "30d", "1mo", "2w", "6h"`, - ); + errors.push(t("bulkCreateUsers.budgetDurationInvalid", { duration: user.budget_duration })); } // Teams validation @@ -240,7 +234,7 @@ const BulkCreateUsersButton: React.FC = ({ const userTeams = user.teams.split(",").map((t) => t.trim()); const invalidTeams = userTeams.filter((t) => !teamIds.includes(t)); if (invalidTeams.length > 0) { - errors.push(`Unknown team(s): ${invalidTeams.join(", ")}`); + errors.push(t("bulkCreateUsers.unknownTeams", { teams: invalidTeams.join(", ") })); } } } @@ -258,24 +252,27 @@ const BulkCreateUsersButton: React.FC = ({ setParsedData(userData); if (userData.length === 0) { - setCsvStructureError("No valid data rows found in the CSV file. Please check your file format."); + setCsvStructureError(t("bulkCreateUsers.noDataRows")); } else if (validData.length === 0) { - setParseError("No valid users found in the CSV. Please check the errors below and fix your CSV file."); + setParseError(t("bulkCreateUsers.noValidUsers")); } else if (validData.length < userData.length) { setParseError( - `Found ${userData.length - validData.length} row(s) with errors out of ${userData.length} total rows. Please correct them before proceeding.`, + t("bulkCreateUsers.someRowsHaveErrors", { + errorCount: userData.length - validData.length, + total: userData.length, + }), ); } else { - NotificationsManager.success(`Successfully parsed ${validData.length} users`); + NotificationsManager.success(t("bulkCreateUsers.parsedSuccess", { count: validData.length })); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - setParseError(`Error parsing CSV: ${errorMessage}`); + setParseError(t("bulkCreateUsers.parseError", { error: errorMessage })); setParsedData([]); } }, error: (error) => { - setParseError(`Failed to parse CSV file: ${error.message}`); + setParseError(t("bulkCreateUsers.parseFileFailed", { error: error.message })); setParsedData([]); }, header: false, @@ -404,7 +401,7 @@ const BulkCreateUsersButton: React.FC = ({ ...u, status: "success", key: response.key || response.user_id, - error: "User created but failed to generate invitation link", + error: t("bulkCreateUsers.invitationLinkFailed"), } : u, ), @@ -412,7 +409,7 @@ const BulkCreateUsersButton: React.FC = ({ } } else { console.log("Error case triggered"); - const errorMessage = response?.error || "Failed to create user"; + const errorMessage = response?.error || t("bulkCreateUsers.failedToCreateUser"); console.log("Error message:", errorMessage); setParsedData((current) => current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)), @@ -459,33 +456,33 @@ const BulkCreateUsersButton: React.FC = ({ const columns = [ { - title: "Row", + title: t("bulkCreateUsers.colRow"), dataIndex: "rowNumber", key: "rowNumber", width: 80, }, { - title: "Email", + title: t("bulkCreateUsers.colEmail"), dataIndex: "user_email", key: "user_email", }, { - title: "Role", + title: t("bulkCreateUsers.colRole"), dataIndex: "user_role", key: "user_role", }, { - title: "Teams", + title: t("bulkCreateUsers.colTeams"), dataIndex: "teams", key: "teams", }, { - title: "Budget", + title: t("bulkCreateUsers.colBudget"), dataIndex: "max_budget", key: "max_budget", }, { - title: "Status", + title: t("common.status"), key: "status", render: (_: any, record: UserData) => { if (!record.isValid) { @@ -493,21 +490,21 @@ const BulkCreateUsersButton: React.FC = ({
- Invalid + {t("bulkCreateUsers.statusInvalid")}
{record.error && {record.error}}
); } if (!record.status || record.status === "pending") { - return Pending; + return {t("bulkCreateUsers.statusPending")}; } if (record.status === "success") { return (
- Success + {t("common.success")}
{record.invitation_link && (
@@ -515,9 +512,9 @@ const BulkCreateUsersButton: React.FC = ({ {record.invitation_link} NotificationsManager.success("Invitation link copied!")} + onCopy={() => NotificationsManager.success(t("bulkCreateUsers.invitationLinkCopied"))} > - +
@@ -529,7 +526,7 @@ const BulkCreateUsersButton: React.FC = ({
- Failed + {t("bulkCreateUsers.statusFailed")}
{record.error && {JSON.stringify(record.error)}}
@@ -541,11 +538,11 @@ const BulkCreateUsersButton: React.FC = ({ return ( <> setIsModalVisible(false)} @@ -560,77 +557,68 @@ const BulkCreateUsersButton: React.FC = ({
1
-

Download and fill the template

+

{t("bulkCreateUsers.step1Title")}

-

Add multiple users at once by following these steps:

+

{t("bulkCreateUsers.step1Desc")}

    -
  1. Download our CSV template
  2. -
  3. Add your users' information to the spreadsheet
  4. -
  5. Save the file and upload it here
  6. -
  7. After creation, download the results file containing the Virtual Keys for each user
  8. +
  9. {t("bulkCreateUsers.step1Item1")}
  10. +
  11. {t("bulkCreateUsers.step1Item2")}
  12. +
  13. {t("bulkCreateUsers.step1Item3")}
  14. +
  15. {t("bulkCreateUsers.step1Item4")}
-

Template Column Names

+

{t("bulkCreateUsers.templateColumnsTitle")}

user_email

-

User's email address (required)

+

{t("bulkCreateUsers.colUserEmailDesc")}

user_role

-

- User's role (one of: "proxy_admin", "proxy_admin_viewer", - "internal_user", "internal_user_viewer") -

+

{t("bulkCreateUsers.colUserRoleDesc")}

teams

-

- Comma-separated team IDs (e.g., "team-1,team-2") -

+

{t("bulkCreateUsers.colTeamsDesc")}

max_budget

-

Maximum budget as a number (e.g., "100")

+

{t("bulkCreateUsers.colMaxBudgetDesc")}

budget_duration

-

- Budget reset period (e.g., "30d", "1mo") -

+

{t("bulkCreateUsers.colBudgetDurationDesc")}

models

-

- Comma-separated allowed models (e.g., "gpt-3.5-turbo,gpt-4") -

+

{t("bulkCreateUsers.colModelsDesc")}

@@ -638,7 +626,7 @@ const BulkCreateUsersButton: React.FC = ({
2
-

Upload your completed CSV

+

{t("bulkCreateUsers.step2Title")}

@@ -668,7 +656,7 @@ const BulkCreateUsersButton: React.FC = ({ className="flex items-center" icon={} > - Remove + {t("common.remove")}
@@ -683,7 +671,7 @@ const BulkCreateUsersButton: React.FC = ({
- Processing... + {t("bulkCreateUsers.processing")} ) )} @@ -692,10 +680,10 @@ const BulkCreateUsersButton: React.FC = ({
-

Drag and drop your CSV file here

-

or

- -

Only CSV files (.csv) are supported

+

{t("bulkCreateUsers.dropzone")}

+

{t("bulkCreateUsers.dropzoneOr")}

+ +

{t("bulkCreateUsers.csvOnly")}

)} @@ -706,13 +694,13 @@ const BulkCreateUsersButton: React.FC = ({
- CSV Structure Error + {t("bulkCreateUsers.csvStructureErrorTitle")} {csvStructureError} - Please download our template and ensure your CSV follows the required format. + {t("bulkCreateUsers.csvStructureErrorHint")}
@@ -728,8 +716,8 @@ const BulkCreateUsersButton: React.FC = ({

{parsedData.some((user) => user.status === "success" || user.status === "failed") - ? "User Creation Results" - : "Review and create users"} + ? t("bulkCreateUsers.step3TitleResults") + : t("bulkCreateUsers.step3TitleReview")}

@@ -741,12 +729,9 @@ const BulkCreateUsersButton: React.FC = ({ {parseError} {parsedData.some((user) => !user.isValid) && (
    -
  • Check the table below for specific errors in each row
  • -
  • - Common issues include invalid email formats, missing required fields, or incorrect role - values -
  • -
  • Fix these issues in your CSV file and upload again
  • +
  • {t("bulkCreateUsers.parseErrorHint1")}
  • +
  • {t("bulkCreateUsers.parseErrorHint2")}
  • +
  • {t("bulkCreateUsers.parseErrorHint3")}
)} @@ -759,21 +744,28 @@ const BulkCreateUsersButton: React.FC = ({
{parsedData.some((user) => user.status === "success" || user.status === "failed") ? (
- Creation Summary + {t("bulkCreateUsers.creationSummary")} - {parsedData.filter((d) => d.status === "success").length} Successful + {t("bulkCreateUsers.successfulCount", { + count: parsedData.filter((d) => d.status === "success").length, + })} {parsedData.some((d) => d.status === "failed") && ( - {parsedData.filter((d) => d.status === "failed").length} Failed + {t("bulkCreateUsers.failedCount", { + count: parsedData.filter((d) => d.status === "failed").length, + })} )}
) : (
- User Preview + {t("bulkCreateUsers.userPreview")} - {parsedData.filter((d) => d.isValid).length} of {parsedData.length} users valid + {t("bulkCreateUsers.validUsersCount", { + valid: parsedData.filter((d) => d.isValid).length, + total: parsedData.length, + })}
)} @@ -787,14 +779,18 @@ const BulkCreateUsersButton: React.FC = ({ setParseError(null); }} > - Back + {t("common.back")}
)} @@ -807,11 +803,10 @@ const BulkCreateUsersButton: React.FC = ({
- User creation complete + {t("bulkCreateUsers.creationComplete")} - Next step: Download the credentials file containing - Virtual Keys and invitation links. Users will need these Virtual Keys to make LLM requests - through LiteLLM. + {t("bulkCreateUsers.nextStep")}{" "} + {t("bulkCreateUsers.nextStepDesc")}
@@ -836,14 +831,18 @@ const BulkCreateUsersButton: React.FC = ({ }} className="mr-3" > - Back + {t("common.back")} )} @@ -857,10 +856,10 @@ const BulkCreateUsersButton: React.FC = ({ }} className="mr-3" > - Start New Bulk Import + {t("bulkCreateUsers.startNewImport")} )} diff --git a/ui/litellm-dashboard/src/components/cache_dashboard.tsx b/ui/litellm-dashboard/src/components/cache_dashboard.tsx index 874cb43276e..619ba670e4f 100644 --- a/ui/litellm-dashboard/src/components/cache_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/cache_dashboard.tsx @@ -25,6 +25,7 @@ import { adminGlobalCacheActivity, cachingHealthCheckCall } from "./networking"; // Import the new component import { CacheHealthTab } from "./cache_health"; import CacheSettings from "./cache_settings"; +import { useTranslation } from "react-i18next"; const formatDateWithoutTZ = (date: Date | undefined) => { if (!date) return undefined; @@ -97,6 +98,7 @@ const deepParse = (input: any) => { }; const CacheDashboard: React.FC = ({ accessToken, token, userRole, userID, premiumUser }) => { + const { t } = useTranslation(); const [filteredData, setFilteredData] = useState([]); const [selectedApiKeys, setSelectedApiKeys] = useState([]); const [selectedModels, setSelectedModels] = useState([]); @@ -160,8 +162,8 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole newData = newData.filter((item) => selectedModels.includes(item.model)); } - /* - Data looks like this + /* + Data looks like this [{"api_key":"sk-test-mock-key-001","call_type":"acompletion","model":"llama3-8b-8192","total_rows":13,"cache_hit_true_rows":0}, {"api_key":"sk-test-mock-key-002","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, {"api_key":"sk-test-mock-key-123","call_type":"acompletion","model":"gpt-3.5-turbo","total_rows":19,"cache_hit_true_rows":0}, @@ -239,7 +241,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole const runCachingHealthCheck = async () => { try { - NotificationsManager.info("Running cache health check..."); + NotificationsManager.info(t("cacheDashboard.runningHealthCheck")); setHealthCheckResponse(""); const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : ""); console.log("CACHING HEALTH CHECK RESPONSE", response); @@ -260,7 +262,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole errorData = { message: error.message }; } } else { - errorData = { message: "Unknown error occurred" }; + errorData = { message: t("cacheDashboard.unknownError") }; } setHealthCheckResponse({ error: errorData }); } @@ -270,13 +272,13 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole
- Cache Analytics - Cache Health - Cache Settings + {t("cacheDashboard.tabAnalytics")} + {t("cacheDashboard.tabHealth")} + {t("cacheDashboard.tabSettings")}
- {lastRefreshed && Last Refreshed: {lastRefreshed}} + {lastRefreshed && {t("cacheDashboard.lastRefreshed", { time: lastRefreshed })}} = ({ accessToken, token, userRole @@ -304,7 +306,11 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole - + {uniqueModels.map((model) => ( {model} @@ -326,7 +332,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole

- Cache Hit Ratio + {t("cacheDashboard.cacheHitRatio")}

@@ -336,7 +342,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole

- Cache Hits + {t("cacheDashboard.cacheHits")}

@@ -347,7 +353,7 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole

- Cached Tokens + {t("cacheDashboard.cachedTokens")}

@@ -357,9 +363,9 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole

- Cache Hits vs API Requests + {t("cacheDashboard.cacheHitsVsApiRequests")} = ({ accessToken, token, userRole yAxisWidth={48} /> - Cached Completion Tokens vs Generated Completion Tokens + {t("cacheDashboard.cachedVsGeneratedTokens")} { @@ -72,6 +73,8 @@ interface ErrorDetails { // Update HealthCheckDetails component to handle errors const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { + const { t } = useTranslation(); + // Initialize with safe default values let errorDetails: ErrorDetails | null = null; let parsedLitellmParams: any = {}; @@ -84,8 +87,8 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { typeof response.error.message === "string" ? JSON.parse(response.error.message) : response.error.message; errorDetails = { - message: errorMessage?.message || "Unknown error", - traceback: errorMessage?.traceback || "No traceback available", + message: errorMessage?.message || t("cacheHealth.unknownError"), + traceback: errorMessage?.traceback || t("cacheHealth.noTraceback"), litellm_params: errorMessage?.litellm_cache_params || {}, health_check_cache_params: errorMessage?.health_check_cache_params || {}, }; @@ -95,8 +98,8 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { } catch (e) { console.warn("Error parsing error details:", e); errorDetails = { - message: String(response.error.message || "Unknown error"), - traceback: "Error parsing details", + message: String(response.error.message || t("cacheHealth.unknownError")), + traceback: t("cacheHealth.errorParsingDetails"), litellm_params: {}, health_check_cache_params: {}, }; @@ -154,8 +157,12 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
- Summary - Raw Response + + {t("cacheHealth.tabSummary")} + + + {t("cacheHealth.tabRawResponse")} + @@ -170,7 +177,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { - Cache Status: {response?.status || "unhealthy"} + {t("cacheHealth.cacheStatus", { status: response?.status || t("cacheHealth.statusUnhealthy") })}
@@ -181,23 +188,32 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { <> - Error Details + {t("cacheHealth.errorDetails")} - - + + )} {/* Always show cache details, regardless of error state */} - Cache Details + {t("cacheHealth.cacheDetails")} - - - + + + = ({ response }) => { <> - Redis Details + {t("cacheHealth.redisDetails")} - - - - - + + + + + )} @@ -249,7 +280,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => { // Then stringify with proper formatting return JSON.stringify(prettyData, null, 2); } catch (e) { - return "Error formatting JSON: " + (e as Error).message; + return t("cacheHealth.jsonFormatError", { error: (e as Error).message }); } })()} @@ -267,6 +298,7 @@ export const CacheHealthTab: React.FC<{ runCachingHealthCheck: () => void; responseTimeMs?: number | null; }> = ({ accessToken, healthCheckResponse, runCachingHealthCheck, responseTimeMs }) => { + const { t } = useTranslation(); const [localResponseTimeMs, setLocalResponseTimeMs] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); @@ -287,7 +319,7 @@ export const CacheHealthTab: React.FC<{ disabled={isLoading} className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white text-sm px-4 py-2 rounded-md" > - {isLoading ? "Running Health Check..." : "Run Health Check"} + {isLoading ? t("cacheHealth.runningHealthCheck") : t("cacheHealth.runHealthCheck")}
diff --git a/ui/litellm-dashboard/src/locales/en.json b/ui/litellm-dashboard/src/locales/en.json index c1fab9b3bf5..a8c36a991ff 100644 --- a/ui/litellm-dashboard/src/locales/en.json +++ b/ui/litellm-dashboard/src/locales/en.json @@ -1009,6 +1009,125 @@ "deleteSuccess": "Agent \"{{name}}\" deleted successfully", "deleteFailed": "Failed to delete agent" }, + "bulkCreateUsers": { + "openButton": "+ Bulk Invite Users", + "modalTitle": "Bulk Invite Users", + "step1Title": "Download and fill the template", + "step1Desc": "Add multiple users at once by following these steps:", + "step1Item1": "Download our CSV template", + "step1Item2": "Add your users' information to the spreadsheet", + "step1Item3": "Save the file and upload it here", + "step1Item4": "After creation, download the results file containing the Virtual Keys for each user", + "templateColumnsTitle": "Template Column Names", + "colUserEmailDesc": "User's email address (required)", + "colUserRoleDesc": "User's role (one of: \"proxy_admin\", \"proxy_admin_viewer\", \"internal_user\", \"internal_user_viewer\")", + "colTeamsDesc": "Comma-separated team IDs (e.g., \"team-1,team-2\")", + "colMaxBudgetDesc": "Maximum budget as a number (e.g., \"100\")", + "colBudgetDurationDesc": "Budget reset period (e.g., \"30d\", \"1mo\")", + "colModelsDesc": "Comma-separated allowed models (e.g., \"gpt-3.5-turbo,gpt-4\")", + "downloadTemplateButton": "Download CSV Template", + "step2Title": "Upload your completed CSV", + "dropzone": "Drag and drop your CSV file here", + "dropzoneOr": "or", + "browseFiles": "Browse files", + "csvOnly": "Only CSV files (.csv) are supported", + "processing": "Processing...", + "csvStructureErrorTitle": "CSV Structure Error", + "csvStructureErrorHint": "Please download our template and ensure your CSV follows the required format.", + "step3TitleResults": "User Creation Results", + "step3TitleReview": "Review and create users", + "parseErrorHint1": "Check the table below for specific errors in each row", + "parseErrorHint2": "Common issues include invalid email formats, missing required fields, or incorrect role values", + "parseErrorHint3": "Fix these issues in your CSV file and upload again", + "creationSummary": "Creation Summary", + "successfulCount_one": "{{count}} Successful", + "successfulCount_other": "{{count}} Successful", + "failedCount_one": "{{count}} Failed", + "failedCount_other": "{{count}} Failed", + "userPreview": "User Preview", + "validUsersCount": "{{valid}} of {{total}} users valid", + "createUsersButton": "Create {{count}} Users", + "creating": "Creating...", + "creationComplete": "User creation complete", + "nextStep": "Next step:", + "nextStepDesc": "Download the credentials file containing Virtual Keys and invitation links. Users will need these Virtual Keys to make LLM requests through LiteLLM.", + "startNewImport": "Start New Bulk Import", + "downloadCredentials": "Download User Credentials", + "colRow": "Row", + "colEmail": "Email", + "colRole": "Role", + "colTeams": "Teams", + "colBudget": "Budget", + "statusInvalid": "Invalid", + "statusPending": "Pending", + "statusFailed": "Failed", + "invitationLinkCopied": "Invitation link copied!", + "invalidFileType": "Invalid file type: {{name}}. Please upload a CSV file (.csv extension).", + "invalidFileTypeShort": "Invalid file type. Please upload a CSV file.", + "fileTooLarge": "File is too large ({{size}} MB). Please upload a CSV file smaller than 5MB.", + "csvEmpty": "The CSV file appears to be empty. Please upload a file with data.", + "csvHeaderOnly": "The CSV file only contains headers but no user data. Please add user data to your CSV.", + "csvNoHeaders": "The CSV file doesn't contain any column headers. Please make sure your CSV has headers.", + "csvMissingColumns": "Your CSV is missing these required columns: {{columns}}. Please add these columns to your CSV file.", + "rowTooFewColumns": "Row {{row}} has fewer columns than the header row. Please ensure all data is properly formatted.", + "emailRequired": "Email is required", + "emailInvalid": "Invalid email format (must contain @ and domain)", + "roleRequired": "Role is required", + "roleInvalid": "Invalid role \"{{role}}\". Must be one of: {{validRoles}}", + "budgetNotNumber": "Max budget \"{{budget}}\" must be a number", + "budgetMustBePositive": "Max budget must be greater than 0", + "budgetDurationInvalid": "Invalid budget duration format \"{{duration}}\". Use format like \"30d\", \"1mo\", \"2w\", \"6h\"", + "unknownTeams": "Unknown team(s): {{teams}}", + "noDataRows": "No valid data rows found in the CSV file. Please check your file format.", + "noValidUsers": "No valid users found in the CSV. Please check the errors below and fix your CSV file.", + "someRowsHaveErrors": "Found {{errorCount}} row(s) with errors out of {{total}} total rows. Please correct them before proceeding.", + "parsedSuccess_one": "Successfully parsed {{count}} user", + "parsedSuccess_other": "Successfully parsed {{count}} users", + "parseError": "Error parsing CSV: {{error}}", + "parseFileFailed": "Failed to parse CSV file: {{error}}", + "invitationLinkFailed": "User created but failed to generate invitation link", + "failedToCreateUser": "Failed to create user" + }, + "cacheDashboard": { + "tabAnalytics": "Cache Analytics", + "tabHealth": "Cache Health", + "tabSettings": "Cache Settings", + "lastRefreshed": "Last Refreshed: {{time}}", + "selectVirtualKeys": "Select Virtual Keys", + "selectModels": "Select Models", + "cacheHitRatio": "Cache Hit Ratio", + "cacheHits": "Cache Hits", + "cachedTokens": "Cached Tokens", + "cacheHitsVsApiRequests": "Cache Hits vs API Requests", + "cachedVsGeneratedTokens": "Cached Completion Tokens vs Generated Completion Tokens", + "runningHealthCheck": "Running cache health check...", + "unknownError": "Unknown error occurred" + }, + "cacheHealth": { + "runHealthCheck": "Run Health Check", + "runningHealthCheck": "Running Health Check...", + "tabSummary": "Summary", + "tabRawResponse": "Raw Response", + "cacheStatus": "Cache Status: {{status}}", + "statusUnhealthy": "unhealthy", + "errorDetails": "Error Details", + "errorMessage": "Error Message", + "traceback": "Traceback", + "cacheDetails": "Cache Details", + "cacheConfiguration": "Cache Configuration", + "pingResponse": "Ping Response", + "setCacheResponse": "Set Cache Response", + "redisDetails": "Redis Details", + "redisHost": "Redis Host", + "redisPort": "Redis Port", + "redisVersion": "Redis Version", + "startupNodes": "Startup Nodes", + "namespace": "Namespace", + "unknownError": "Unknown error", + "noTraceback": "No traceback available", + "errorParsingDetails": "Error parsing details", + "jsonFormatError": "Error formatting JSON: {{error}}" + }, "common": { "save": "Save", "cancel": "Cancel", diff --git a/ui/litellm-dashboard/src/locales/zh-CN.json b/ui/litellm-dashboard/src/locales/zh-CN.json index d52a82f4bfc..4ac5f95c536 100644 --- a/ui/litellm-dashboard/src/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/locales/zh-CN.json @@ -1009,6 +1009,125 @@ "deleteSuccess": "Agent「{{name}}」已成功删除", "deleteFailed": "删除 Agent 失败" }, + "bulkCreateUsers": { + "openButton": "+ 批量邀请用户", + "modalTitle": "批量邀请用户", + "step1Title": "下载并填写模板", + "step1Desc": "按照以下步骤批量添加用户:", + "step1Item1": "下载 CSV 模板", + "step1Item2": "在表格中填写用户信息", + "step1Item3": "保存文件并上传", + "step1Item4": "创建完成后,下载包含每位用户虚拟密钥的结果文件", + "templateColumnsTitle": "模板列名说明", + "colUserEmailDesc": "用户邮箱地址(必填)", + "colUserRoleDesc": "用户角色(可选:\"proxy_admin\"、\"proxy_admin_viewer\"、\"internal_user\"、\"internal_user_viewer\")", + "colTeamsDesc": "逗号分隔的团队 ID(如:\"team-1,team-2\")", + "colMaxBudgetDesc": "最大预算(数字,如:\"100\")", + "colBudgetDurationDesc": "预算重置周期(如:\"30d\"、\"1mo\")", + "colModelsDesc": "逗号分隔的允许模型(如:\"gpt-3.5-turbo,gpt-4\")", + "downloadTemplateButton": "下载 CSV 模板", + "step2Title": "上传填写好的 CSV", + "dropzone": "将 CSV 文件拖拽到此处", + "dropzoneOr": "或", + "browseFiles": "浏览文件", + "csvOnly": "仅支持 CSV 文件(.csv 格式)", + "processing": "处理中...", + "csvStructureErrorTitle": "CSV 结构错误", + "csvStructureErrorHint": "请下载我们的模板,并确保您的 CSV 符合所需格式。", + "step3TitleResults": "用户创建结果", + "step3TitleReview": "预览并创建用户", + "parseErrorHint1": "查看下方表格,了解每行的具体错误", + "parseErrorHint2": "常见问题包括邮箱格式无效、缺少必填字段或角色值不正确", + "parseErrorHint3": "在 CSV 文件中修正这些问题后重新上传", + "creationSummary": "创建摘要", + "successfulCount_one": "{{count}} 个成功", + "successfulCount_other": "{{count}} 个成功", + "failedCount_one": "{{count}} 个失败", + "failedCount_other": "{{count}} 个失败", + "userPreview": "用户预览", + "validUsersCount": "{{valid}} / {{total}} 个用户有效", + "createUsersButton": "创建 {{count}} 个用户", + "creating": "创建中...", + "creationComplete": "用户创建完成", + "nextStep": "下一步:", + "nextStepDesc": "下载包含虚拟密钥和邀请链接的凭据文件。用户需要这些虚拟密钥通过 LiteLLM 发起 LLM 请求。", + "startNewImport": "开始新的批量导入", + "downloadCredentials": "下载用户凭据", + "colRow": "行号", + "colEmail": "邮箱", + "colRole": "角色", + "colTeams": "团队", + "colBudget": "预算", + "statusInvalid": "无效", + "statusPending": "待处理", + "statusFailed": "失败", + "invitationLinkCopied": "邀请链接已复制!", + "invalidFileType": "文件类型无效:{{name}}。请上传 CSV 文件(.csv 格式)。", + "invalidFileTypeShort": "文件类型无效,请上传 CSV 文件。", + "fileTooLarge": "文件过大({{size}} MB),请上传小于 5MB 的 CSV 文件。", + "csvEmpty": "CSV 文件为空,请上传包含数据的文件。", + "csvHeaderOnly": "CSV 文件仅包含表头,没有用户数据,请添加用户数据。", + "csvNoHeaders": "CSV 文件不包含列名,请确保 CSV 文件包含列名。", + "csvMissingColumns": "CSV 文件缺少以下必填列:{{columns}}。请添加这些列。", + "rowTooFewColumns": "第 {{row}} 行的列数少于表头行,请确保所有数据格式正确。", + "emailRequired": "邮箱为必填项", + "emailInvalid": "邮箱格式无效(必须包含 @ 和域名)", + "roleRequired": "角色为必填项", + "roleInvalid": "角色「{{role}}」无效,必须为以下之一:{{validRoles}}", + "budgetNotNumber": "最大预算「{{budget}}」必须为数字", + "budgetMustBePositive": "最大预算必须大于 0", + "budgetDurationInvalid": "预算周期格式「{{duration}}」无效,请使用如「30d」、「1mo」、「2w」、「6h」等格式", + "unknownTeams": "未知团队:{{teams}}", + "noDataRows": "CSV 文件中未找到有效数据行,请检查文件格式。", + "noValidUsers": "CSV 中未找到有效用户,请查看下方错误并修正 CSV 文件。", + "someRowsHaveErrors": "共 {{total}} 行中有 {{errorCount}} 行存在错误,请修正后再继续。", + "parsedSuccess_one": "成功解析 {{count}} 个用户", + "parsedSuccess_other": "成功解析 {{count}} 个用户", + "parseError": "解析 CSV 时出错:{{error}}", + "parseFileFailed": "解析 CSV 文件失败:{{error}}", + "invitationLinkFailed": "用户已创建,但生成邀请链接失败", + "failedToCreateUser": "创建用户失败" + }, + "cacheDashboard": { + "tabAnalytics": "缓存分析", + "tabHealth": "缓存健康", + "tabSettings": "缓存设置", + "lastRefreshed": "最后刷新:{{time}}", + "selectVirtualKeys": "选择虚拟密钥", + "selectModels": "选择模型", + "cacheHitRatio": "缓存命中率", + "cacheHits": "缓存命中数", + "cachedTokens": "已缓存 Token 数", + "cacheHitsVsApiRequests": "缓存命中 vs API 请求", + "cachedVsGeneratedTokens": "已缓存 Token vs 生成 Token", + "runningHealthCheck": "正在运行缓存健康检查...", + "unknownError": "发生未知错误" + }, + "cacheHealth": { + "runHealthCheck": "运行健康检查", + "runningHealthCheck": "健康检查中...", + "tabSummary": "摘要", + "tabRawResponse": "原始响应", + "cacheStatus": "缓存状态:{{status}}", + "statusUnhealthy": "不健康", + "errorDetails": "错误详情", + "errorMessage": "错误信息", + "traceback": "Traceback", + "cacheDetails": "缓存详情", + "cacheConfiguration": "缓存配置", + "pingResponse": "Ping 响应", + "setCacheResponse": "写缓存响应", + "redisDetails": "Redis 详情", + "redisHost": "Redis 主机", + "redisPort": "Redis 端口", + "redisVersion": "Redis 版本", + "startupNodes": "启动节点", + "namespace": "命名空间", + "unknownError": "未知错误", + "noTraceback": "无 Traceback 信息", + "errorParsingDetails": "解析详情时出错", + "jsonFormatError": "JSON 格式化出错:{{error}}" + }, "common": { "save": "保存", "cancel": "取消",