diff --git a/ui/litellm-dashboard/src/components/ToolPolicies.tsx b/ui/litellm-dashboard/src/components/ToolPolicies.tsx index 9db693c8918..5d2e948f593 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button, Switch, Tooltip } from "antd"; import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; import { TimeCell } from "./view_logs/time_cell"; @@ -29,11 +30,15 @@ function countToolsInUTCDay(tools: ToolRow[], utcDateKey: string): number { return tools.filter((t) => isCreatedInUTCDay(t.created_at, utcDateKey)).length; } -function getTrendSubtitle(newToday: number, newYesterday: number): string | undefined { +function getTrendSubtitle( + newToday: number, + newYesterday: number, + t: (key: string, opts?: Record) => string, +): string | undefined { const diff = newToday - newYesterday; if (diff === 0) return undefined; - if (diff > 0) return `+${diff} since yesterday`; - return `${diff} since yesterday`; + if (diff > 0) return t("toolPolicies.trendPositive", { diff }); + return t("toolPolicies.trendNegative", { diff }); } type SortField = "tool_name" | "input_policy" | "output_policy" | "team_id" | "key_alias" | "created_at" | "call_count"; @@ -49,6 +54,7 @@ interface ToolPoliciesProps { } export const ToolPolicies: React.FC = ({ accessToken, onSelectTool }) => { + const { t } = useTranslation(); const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [isFetching, setIsFetching] = useState(false); @@ -75,12 +81,12 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const rows = await fetchToolsList(accessToken); setTools(rows); } catch (e: any) { - setError(e.message ?? "Failed to load tools"); + setError(e.message ?? t("toolPolicies.failedToLoadTools")); } finally { setIsFetching(false); setLoading(false); } - }, [accessToken]); + }, [accessToken, t]); useEffect(() => { load(); @@ -99,7 +105,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, input_policy: newPolicy } : t))); } catch (e: any) { - alert(`Failed to update input policy: ${e.message}`); + alert(t("toolPolicies.updateInputPolicyFailed", { error: e.message })); } finally { setSavingInput(null); } @@ -112,7 +118,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, output_policy: newPolicy } : t))); } catch (e: any) { - alert(`Failed to update output policy: ${e.message}`); + alert(t("toolPolicies.updateOutputPolicyFailed", { error: e.message })); } finally { setSavingOutput(null); } @@ -151,22 +157,22 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const filterOptions: FilterOption[] = [ { name: "Input Policy", - label: "Input Policy", + label: t("toolPolicies.inputPolicy"), options: INPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), }, { name: "Output Policy", - label: "Output Policy", + label: t("toolPolicies.outputPolicy"), options: OUTPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), }, { name: "Team Name", - label: "Team Name", + label: t("toolPolicies.teamName"), options: teamOptions, }, { name: "Key Name", - label: "Key Name", + label: t("toolPolicies.keyName"), options: keyAliasOptions, }, ]; @@ -181,7 +187,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const newToday = countToolsInUTCDay(tools, todayKey); const newYesterday = countToolsInUTCDay(tools, yesterdayKey); - const trendSubtitle = getTrendSubtitle(newToday, newYesterday); + const trendSubtitle = getTrendSubtitle(newToday, newYesterday, t); const totalTools = tools.length; const blockedCount = tools.filter((t) => t.input_policy === "blocked").length; @@ -200,7 +206,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec activeTeamsCount, needsReviewTools, }; - }, [tools]); + }, [tools, t]); const SortHeader = ({ label, field }: { label: string; field: SortField }) => (
@@ -257,11 +263,11 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec return (
-

Tool Policies

+

{t("toolPolicies.title")}

= ({ accessToken, onSelec } /> - + 0 ? "text-red-600" : undefined} /> - 0 ? activeTeamsCount : "—"} /> + 0 ? activeTeamsCount : "—"} />
{needsReviewTools.length > 0 && (
-

Needs Review

+

{t("toolPolicies.needsReview")}

- {needsReviewTools.length} new tool{needsReviewTools.length !== 1 ? "s" : ""} discovered that require policy - decisions. + {t("toolPolicies.needsReviewDesc", { count: needsReviewTools.length })}

- {needsReviewTools.map((t) => ( + {needsReviewTools.map((tool) => ( - - {t.tool_name} + + {tool.tool_name} ))} @@ -316,7 +321,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
{ @@ -340,7 +345,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
- Live Tail + {t("toolPolicies.liveTail")}
@@ -362,32 +367,33 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> - {isButtonLoading ? "Fetching" : "Fetch"} + {isButtonLoading ? t("toolPolicies.fetching") : t("toolPolicies.fetch")}
- Showing {filtered.length === 0 ? 0 : (currentPage - 1) * pageSize + 1} -{" "} - {Math.min(currentPage * pageSize, filtered.length)} of {filtered.length} results - - - Page {currentPage} of {totalPages} + {t("toolPolicies.showing", { + from: filtered.length === 0 ? 0 : (currentPage - 1) * pageSize + 1, + to: Math.min(currentPage * pageSize, filtered.length), + total: filtered.length, + })} + {t("toolPolicies.page", { current: currentPage, total: totalPages })}
@@ -398,16 +404,16 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec options={filterOptions} onApplyFilters={handleApplyFilters} onResetFilters={handleResetFilters} - buttonLabel="Filters" + buttonLabel={t("toolPolicies.filtersButton")} />
{isLiveTail && (
- Auto-refreshing every 15 seconds + {t("toolPolicies.autoRefreshing")}
)} @@ -420,41 +426,41 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec - + - + - + - + - + - + - Key Hash + {t("toolPolicies.colKeyHash")} - + - User Agent + {t("toolPolicies.colUserAgent")} {loading ? ( - Loading tools… + {t("toolPolicies.loadingTools")} ) : paginated.length === 0 ? ( - No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery. + {t("toolPolicies.noToolsDiscovered")} ) : ( @@ -469,7 +475,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec onClick={() => onSelectTool?.(tool.tool_name)} className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0" > - + {tool.tool_name} @@ -530,8 +536,11 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec {totalPages > 1 && (
- Showing {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, sorted.length)} of{" "} - {sorted.length} + {t("toolPolicies.showing", { + from: (currentPage - 1) * pageSize + 1, + to: Math.min(currentPage * pageSize, sorted.length), + total: sorted.length, + })}
diff --git a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx index 77bee2c1e52..db13b053822 100644 --- a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx +++ b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Form, Button as Button2, Select } from "antd"; import { Text, TextInput } from "@tremor/react"; import { getSSOSettings, updateSSOSettings } from "./networking"; @@ -11,6 +12,7 @@ interface UIAccessControlFormProps { // Separate UI Access Control Form Component const UIAccessControlForm: React.FC = ({ accessToken, onSuccess }) => { + const { t } = useTranslation(); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); @@ -53,7 +55,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, const handleUIAccessSubmit = async (formValues: Record) => { if (!accessToken) { - NotificationManager.fromBackend("No access token available"); + NotificationManager.fromBackend(t("uiAccessControlForm.noAccessToken")); return; } @@ -81,7 +83,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, onSuccess(); } catch (error) { console.error("Failed to save UI access settings:", error); - NotificationManager.fromBackend("Failed to save UI access settings"); + NotificationManager.fromBackend(t("uiAccessControlForm.saveSettingsFailed")); } finally { setLoading(false); } @@ -90,16 +92,22 @@ const UIAccessControlForm: React.FC = ({ accessToken, return (
- - Configure who can access the UI interface and how group information is extracted from JWT tokens. - + {t("uiAccessControlForm.description")}
- - + + {t("uiAccessControlForm.optionAllAuthenticatedUsers")} + + + {t("uiAccessControlForm.optionRestrictedSsoGroup")} + @@ -113,9 +121,9 @@ const UIAccessControlForm: React.FC = ({ accessToken, const uiAccessModeType = getFieldValue("ui_access_mode_type"); return uiAccessModeType === "restricted_sso_group" ? ( @@ -124,9 +132,9 @@ const UIAccessControlForm: React.FC = ({ accessToken, @@ -141,7 +149,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, borderColor: "#6366f1", }} > - Update UI Access Control + {t("uiAccessControlForm.updateButton")}
diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.tsx index 3052bf645c6..c02afdab581 100644 --- a/ui/litellm-dashboard/src/components/UsageIndicator.tsx +++ b/ui/litellm-dashboard/src/components/UsageIndicator.tsx @@ -12,6 +12,7 @@ import { Users, } from "lucide-react"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getRemainingUsers, getLicenseInfo, LicenseInfo } from "./networking"; // Simple utility function to combine class names @@ -44,19 +45,21 @@ const getDaysUntilExpiration = (expirationDate: string | null): number | null => return diffDays; }; -// Format expiration for display -const formatExpirationDisplay = (daysRemaining: number | null): string => { - if (daysRemaining === null) return "No expiration"; - if (daysRemaining < 0) return "Expired"; - if (daysRemaining === 0) return "Expires today"; - if (daysRemaining === 1) return "1 day remaining"; - if (daysRemaining < 30) return `${daysRemaining} days remaining`; - if (daysRemaining < 60) return "1 month remaining"; +const formatExpirationDisplay = ( + daysRemaining: number | null, + t: (key: string, opts?: Record) => string, +): string => { + if (daysRemaining === null) return t("usageIndicator.noExpiration"); + if (daysRemaining < 0) return t("usageIndicator.expired"); + if (daysRemaining === 0) return t("usageIndicator.expiresToday"); + if (daysRemaining < 30) return t("usageIndicator.daysRemaining", { count: daysRemaining }); + if (daysRemaining < 60) return t("usageIndicator.oneMonthRemaining"); const months = Math.floor(daysRemaining / 30); - return `${months} months remaining`; + return t("usageIndicator.monthsRemaining", { count: months }); }; export default function UsageIndicator({ accessToken, width = 220 }: UsageIndicatorProps) { + const { t } = useTranslation(); const disableUsageIndicator = useDisableUsageIndicator(); const [isExpanded, setIsExpanded] = useState(false); const [isMinimized, setIsMinimized] = useState(false); @@ -81,7 +84,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica setLicenseInfo(licenseResult); } catch (err) { console.error("Failed to fetch usage data:", err); - setError("Failed to load usage data"); + setError(t("usageIndicator.failedToLoad")); } finally { setIsLoading(false); } @@ -177,7 +180,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica hasError && "text-red-400 hover:text-red-600", hasWarning && "text-yellow-500 hover:text-yellow-700", )} - title="Show usage details" + title={t("usageIndicator.showUsageDetails")} > {hasAnyIssue && {getStatusIcon()}} @@ -200,12 +203,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica isLicenseExpiringSoon && "text-yellow-500", )} > - {daysUntilExpiration < 0 ? "Exp!" : `${daysUntilExpiration}d`} + {daysUntilExpiration < 0 ? t("usageIndicator.expiredShort") : `${daysUntilExpiration}d`} )} {!data || (data.total_users === null && data.total_teams === null && !licenseInfo && ( - Usage + {t("usageIndicator.usageLabel")} ))}
@@ -223,7 +226,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica return (
- Loading... + {t("common.loading")}
); } @@ -236,12 +239,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica >
- {error || "No data"} + {error || t("common.noData")}
@@ -262,7 +265,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica )} > - Usage Status + {t("usageIndicator.usageStatus")} {hasAnyIssue && ( {getStatusIcon()} @@ -279,7 +282,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica @@ -293,7 +296,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
- License + {t("usageIndicator.licenseLabel")}
) : null} - {formatExpirationDisplay(daysUntilExpiration)} + {formatExpirationDisplay(daysUntilExpiration, t)}
)} @@ -320,7 +323,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica {data.total_users_used}/{data.total_users} - users + {t("usageIndicator.usersLabel")} {/* User progress bar */} @@ -349,7 +352,11 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica ) : ( )} - Users {userMetrics.isOverLimit ? "Over Limit" : "Near Limit"} + + {userMetrics.isOverLimit + ? t("usageIndicator.usersOverLimit") + : t("usageIndicator.usersNearLimit")} + )} @@ -363,7 +370,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica {data.total_teams_used}/{data.total_teams} - teams + {t("usageIndicator.teamsLabel")} {/* Team progress bar */} @@ -392,7 +399,11 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica ) : ( )} - Teams {teamMetrics.isOverLimit ? "Over Limit" : "Near Limit"} + + {teamMetrics.isOverLimit + ? t("usageIndicator.teamsOverLimit") + : t("usageIndicator.teamsNearLimit")} + )} @@ -412,7 +423,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica className={cn( "bg-white border border-gray-200 rounded-lg shadow-sm p-3 hover:shadow-md transition-all w-full", )} - title="Show usage details" + title={t("usageIndicator.showUsageDetails")} >
@@ -451,12 +462,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica !isLicenseExpired && !isLicenseExpiringSoon && "bg-gray-50 text-gray-700 border-gray-200", )} > - {daysUntilExpiration < 0 ? "Exp!" : `${daysUntilExpiration}d`} + {daysUntilExpiration < 0 ? t("usageIndicator.expiredShort") : `${daysUntilExpiration}d`} )} {!data || (data.total_users === null && data.total_teams === null && !licenseInfo && ( - Usage + {t("usageIndicator.usageLabel")} ))}
@@ -469,7 +480,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
- Loading... + {t("common.loading")}
); @@ -480,12 +491,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
- {error || "No data"} + {error || t("common.noData")}
@@ -499,12 +510,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
- Usage + {t("usageIndicator.usageLabel")}
@@ -523,7 +534,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica >
- License + {t("usageIndicator.licenseLabel")} - {isLicenseExpired ? "Expired" : isLicenseExpiringSoon ? "Expiring soon" : "OK"} + {isLicenseExpired + ? t("usageIndicator.expired") + : isLicenseExpiringSoon + ? t("usageIndicator.expiringSoon") + : t("common.ok")}
- Status: + {t("usageIndicator.statusLabel")} - {formatExpirationDisplay(daysUntilExpiration)} + {formatExpirationDisplay(daysUntilExpiration, t)}
{licenseInfo.license_type && (
- Type: + {t("common.type")}: {licenseInfo.license_type}
)} @@ -567,7 +582,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica >
- Users + {t("usageIndicator.usersSection")} - {userMetrics.isOverLimit ? "Over limit" : userMetrics.isNearLimit ? "Near limit" : "OK"} + {userMetrics.isOverLimit + ? t("usageIndicator.overLimit") + : userMetrics.isNearLimit + ? t("usageIndicator.nearLimit") + : t("common.ok")}
- Used: + {t("usageIndicator.usedLabel")} {data.total_users_used}/{data.total_users}
- Remaining: + {t("usageIndicator.remainingLabel")}
- Usage: + {t("usageIndicator.usageRowLabel")} {Math.round(userMetrics.usagePercentage)}%
@@ -628,7 +647,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica >
- Teams + {t("usageIndicator.teamsSection")} - {teamMetrics.isOverLimit ? "Over limit" : teamMetrics.isNearLimit ? "Near limit" : "OK"} + {teamMetrics.isOverLimit + ? t("usageIndicator.overLimit") + : teamMetrics.isNearLimit + ? t("usageIndicator.nearLimit") + : t("common.ok")}
- Used: + {t("usageIndicator.usedLabel")} {data.total_teams_used}/{data.total_teams}
- Remaining: + {t("usageIndicator.remainingLabel")}
- Usage: + {t("usageIndicator.usageRowLabel")} {Math.round(teamMetrics.usagePercentage)}%
diff --git a/ui/litellm-dashboard/src/locales/en.json b/ui/litellm-dashboard/src/locales/en.json index 9e491110987..8ab54995202 100644 --- a/ui/litellm-dashboard/src/locales/en.json +++ b/ui/litellm-dashboard/src/locales/en.json @@ -838,6 +838,90 @@ "addOverrideFailed": "Failed to add override: {{error}}", "removeOverrideFailed": "Failed to remove override: {{error}}" }, + "toolPolicies": { + "title": "Tool Policies", + "newToday": "New Today", + "totalToolsDiscovered": "Total Tools Discovered", + "blockedTools": "Blocked Tools", + "activeTeams": "Active Teams", + "needsReview": "Needs Review", + "needsReviewDesc_one": "{{count}} new tool discovered that requires a policy decision.", + "needsReviewDesc_other": "{{count}} new tools discovered that require policy decisions.", + "review": "Review", + "searchByToolName": "Search by Tool Name", + "liveTail": "Live Tail", + "fetch": "Fetch", + "fetching": "Fetching", + "showing": "Showing {{from}} - {{to}} of {{total}} results", + "page": "Page {{current}} of {{total}}", + "filtersButton": "Filters", + "autoRefreshing": "Auto-refreshing every 15 seconds", + "stop": "Stop", + "colDiscovered": "Discovered", + "colToolName": "Tool Name", + "inputPolicy": "Input Policy", + "outputPolicy": "Output Policy", + "colCalls": "# Calls", + "teamName": "Team Name", + "colKeyHash": "Key Hash", + "keyName": "Key Name", + "colUserAgent": "User Agent", + "loadingTools": "Loading tools…", + "noToolsDiscovered": "No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery.", + "clickToViewDetails": "Click to view details and block for team/key", + "trendPositive": "+{{diff}} since yesterday", + "trendNegative": "{{diff}} since yesterday", + "updateInputPolicyFailed": "Failed to update input policy: {{error}}", + "updateOutputPolicyFailed": "Failed to update output policy: {{error}}", + "failedToLoadTools": "Failed to load tools" + }, + "uiAccessControlForm": { + "description": "Configure who can access the UI interface and how group information is extracted from JWT tokens.", + "uiAccessModeLabel": "UI Access Mode", + "uiAccessModeTooltip": "Controls who can access the UI interface", + "selectAccessModePlaceholder": "Select access mode", + "optionAllAuthenticatedUsers": "All Authenticated Users", + "optionRestrictedSsoGroup": "Restricted SSO Group", + "restrictedSsoGroupLabel": "Restricted SSO Group", + "restrictedSsoGroupRequired": "Please enter the restricted SSO group", + "ssoGroupJwtFieldLabel": "SSO Group JWT Field", + "ssoGroupJwtFieldTooltip": "JWT field name that contains team/group information. Use dot notation to access nested fields.", + "updateButton": "Update UI Access Control", + "noAccessToken": "No access token available", + "saveSettingsFailed": "Failed to save UI access settings" + }, + "usageIndicator": { + "showUsageDetails": "Show usage details", + "minimize": "Minimize", + "usageLabel": "Usage", + "usageStatus": "Usage Status", + "failedToLoad": "Failed to load usage data", + "expiredShort": "Exp!", + "licenseLabel": "License", + "usersLabel": "users", + "teamsLabel": "teams", + "usersSection": "Users", + "teamsSection": "Teams", + "usersOverLimit": "Users Over Limit", + "usersNearLimit": "Users Near Limit", + "teamsOverLimit": "Teams Over Limit", + "teamsNearLimit": "Teams Near Limit", + "overLimit": "Over limit", + "nearLimit": "Near limit", + "statusLabel": "Status:", + "usedLabel": "Used:", + "remainingLabel": "Remaining:", + "usageRowLabel": "Usage:", + "noExpiration": "No expiration", + "expired": "Expired", + "expiresToday": "Expires today", + "daysRemaining_one": "{{count}} day remaining", + "daysRemaining_other": "{{count}} days remaining", + "oneMonthRemaining": "1 month remaining", + "monthsRemaining_one": "{{count}} month remaining", + "monthsRemaining_other": "{{count}} months remaining", + "expiringSoon": "Expiring soon" + }, "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 b65f81595e3..a2d5c9f30a0 100644 --- a/ui/litellm-dashboard/src/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/locales/zh-CN.json @@ -838,6 +838,90 @@ "addOverrideFailed": "添加覆盖规则失败:{{error}}", "removeOverrideFailed": "移除覆盖规则失败:{{error}}" }, + "toolPolicies": { + "title": "工具策略", + "newToday": "今日新增", + "totalToolsDiscovered": "已发现工具总数", + "blockedTools": "已屏蔽工具", + "activeTeams": "活跃团队", + "needsReview": "待审核", + "needsReviewDesc_one": "发现 {{count}} 个新工具,需要制定策略。", + "needsReviewDesc_other": "发现 {{count}} 个新工具,需要制定策略。", + "review": "审核", + "searchByToolName": "按工具名称搜索", + "liveTail": "实时追踪", + "fetch": "获取", + "fetching": "获取中", + "showing": "显示第 {{from}} - {{to}} 条,共 {{total}} 条", + "page": "第 {{current}} 页,共 {{total}} 页", + "filtersButton": "筛选", + "autoRefreshing": "每 15 秒自动刷新", + "stop": "停止", + "colDiscovered": "发现时间", + "colToolName": "工具名称", + "inputPolicy": "输入策略", + "outputPolicy": "输出策略", + "colCalls": "调用次数", + "teamName": "团队名称", + "colKeyHash": "密钥哈希", + "keyName": "密钥名称", + "colUserAgent": "User Agent", + "loadingTools": "加载工具中…", + "noToolsDiscovered": "尚未发现工具。请发起一次返回 tool_calls 的对话补全以启动自动发现。", + "clickToViewDetails": "点击查看详情并为团队/密钥屏蔽", + "trendPositive": "较昨日 +{{diff}}", + "trendNegative": "较昨日 {{diff}}", + "updateInputPolicyFailed": "更新输入策略失败:{{error}}", + "updateOutputPolicyFailed": "更新输出策略失败:{{error}}", + "failedToLoadTools": "加载工具失败" + }, + "uiAccessControlForm": { + "description": "配置谁可以访问 UI 界面,以及如何从 JWT Token 中提取用户组信息。", + "uiAccessModeLabel": "UI 访问模式", + "uiAccessModeTooltip": "控制谁可以访问 UI 界面", + "selectAccessModePlaceholder": "选择访问模式", + "optionAllAuthenticatedUsers": "所有已认证用户", + "optionRestrictedSsoGroup": "受限 SSO 用户组", + "restrictedSsoGroupLabel": "受限 SSO 用户组", + "restrictedSsoGroupRequired": "请输入受限 SSO 用户组", + "ssoGroupJwtFieldLabel": "SSO 用户组 JWT 字段", + "ssoGroupJwtFieldTooltip": "包含团队/用户组信息的 JWT 字段名称。使用点号表示法访问嵌套字段。", + "updateButton": "更新界面访问控制", + "noAccessToken": "无可用访问令牌", + "saveSettingsFailed": "保存界面访问设置失败" + }, + "usageIndicator": { + "showUsageDetails": "显示用量详情", + "minimize": "最小化", + "usageLabel": "用量", + "usageStatus": "用量状态", + "failedToLoad": "加载用量数据失败", + "expiredShort": "已过期!", + "licenseLabel": "许可证", + "usersLabel": "用户", + "teamsLabel": "团队", + "usersSection": "用户", + "teamsSection": "团队", + "usersOverLimit": "用户超出限制", + "usersNearLimit": "用户接近限制", + "teamsOverLimit": "团队超出限制", + "teamsNearLimit": "团队接近限制", + "overLimit": "超出限制", + "nearLimit": "接近限制", + "statusLabel": "状态:", + "usedLabel": "已用:", + "remainingLabel": "剩余:", + "usageRowLabel": "用量:", + "noExpiration": "无过期时间", + "expired": "已过期", + "expiresToday": "今日过期", + "daysRemaining_one": "剩余 {{count}} 天", + "daysRemaining_other": "剩余 {{count}} 天", + "oneMonthRemaining": "剩余 1 个月", + "monthsRemaining_one": "剩余 {{count}} 个月", + "monthsRemaining_other": "剩余 {{count}} 个月", + "expiringSoon": "即将过期" + }, "common": { "save": "保存", "cancel": "取消",