diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 602490a0c98..676995589c0 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { organizationKeys, useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useQueryClient } from "@tanstack/react-query"; @@ -55,12 +56,7 @@ import RouterSettingsAccordion, { RouterSettingsAccordionRef } from "../common_c import MemberModal from "./EditMembership"; import MemberPermissions from "./member_permissions"; import MyUserTab from "./MyUserTab"; -import { - getTeamInfoDefaultTab, - getTeamInfoVisibleTabs, - TEAM_INFO_TAB_KEYS, - TEAM_INFO_TAB_LABELS, -} from "./tabVisibilityUtils"; +import { getTeamInfoDefaultTab, getTeamInfoVisibleTabs, TEAM_INFO_TAB_KEYS } from "./tabVisibilityUtils"; import TeamMembersComponent from "./TeamMemberTab"; import { TeamVirtualKeysTable } from "./TeamVirtualKeysTable"; @@ -187,6 +183,7 @@ const TeamInfoView: React.FC = ({ premiumUser = false, onUpdate, }) => { + const { t } = useTranslation(); const [teamData, setTeamData] = useState(null); const [loading, setLoading] = useState(true); const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); @@ -243,7 +240,7 @@ const TeamInfoView: React.FC = ({ const response = await teamInfoCall(accessToken, teamId); setTeamData(response); } catch (error) { - NotificationsManager.fromBackend("Failed to load team information"); + NotificationsManager.fromBackend(t("teamPage.teamInfo.failedToLoadTeam")); console.error("Error fetching team info:", error); } finally { setLoading(false); @@ -351,7 +348,7 @@ const TeamInfoView: React.FC = ({ await teamMemberAddCall(accessToken, teamId, member); - NotificationsManager.success("Team member added successfully"); + NotificationsManager.success(t("teamPage.teamInfo.memberAddedSuccess")); setIsAddMemberModalVisible(false); form.resetFields(); @@ -362,10 +359,10 @@ const TeamInfoView: React.FC = ({ // Notify parent component of the update onUpdate(updatedTeamData); } catch (error: any) { - let errMsg = "Failed to add team member"; + let errMsg = t("teamPage.teamInfo.failedToAddMember"); if (error?.raw?.detail?.error?.includes("Assigning team admins is a premium feature")) { - errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; + errMsg = t("teamPage.teamInfo.assignAdminsEnterprise"); } else if (error?.message) { errMsg = error.message; } @@ -395,7 +392,7 @@ const TeamInfoView: React.FC = ({ await teamMemberUpdateCall(accessToken, teamId, member); - NotificationsManager.success("Team member updated successfully"); + NotificationsManager.success(t("teamPage.teamInfo.memberUpdatedSuccess")); setIsEditMemberModalVisible(false); // Fetch updated team info @@ -405,9 +402,9 @@ const TeamInfoView: React.FC = ({ // Notify parent component of the update onUpdate(updatedTeamData); } catch (error: any) { - let errMsg = "Failed to update team member"; + let errMsg = t("teamPage.teamInfo.failedToUpdateMember"); if (error?.raw?.detail?.includes("Assigning team admins is a premium feature")) { - errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; + errMsg = t("teamPage.teamInfo.assignAdminsEnterprise"); } else if (error?.message) { errMsg = error.message; } @@ -432,7 +429,7 @@ const TeamInfoView: React.FC = ({ try { await teamMemberDeleteCall(accessToken, teamId, memberToDelete); - NotificationsManager.success("Team member removed successfully"); + NotificationsManager.success(t("teamPage.teamInfo.memberRemovedSuccess")); // Fetch updated team info const updatedTeamData = await teamInfoCall(accessToken, teamId); @@ -441,7 +438,7 @@ const TeamInfoView: React.FC = ({ // Notify parent component of the update onUpdate(updatedTeamData); } catch (error) { - NotificationsManager.fromBackend("Failed to remove team member"); + NotificationsManager.fromBackend(t("teamPage.teamInfo.failedToRemoveMember")); console.error("Error removing team member:", error); } finally { setIsDeleting(false); @@ -467,7 +464,7 @@ const TeamInfoView: React.FC = ({ const { soft_budget_alerting_emails, ...rest } = rawMetadata; parsedMetadata = rest; } catch (e) { - NotificationsManager.fromBackend("Invalid JSON in metadata field"); + NotificationsManager.fromBackend(t("teamPage.teamInfo.invalidMetadataJson")); return; } @@ -478,7 +475,7 @@ const TeamInfoView: React.FC = ({ try { secretManagerSettings = JSON.parse(values.secret_manager_settings); } catch (e) { - NotificationsManager.fromBackend("Invalid JSON in secret manager settings"); + NotificationsManager.fromBackend(t("teamPage.teamInfo.invalidSecretManagerJson")); return; } } @@ -641,7 +638,7 @@ const TeamInfoView: React.FC = ({ const response = await teamUpdateCall(accessToken, updateData); queryClient.invalidateQueries({ queryKey: organizationKeys.all }); - NotificationsManager.success("Team settings updated successfully"); + NotificationsManager.success(t("teamPage.teamInfo.teamSettingsUpdatedSuccess")); setIsEditing(false); fetchTeamInfo(); } catch (error) { @@ -652,11 +649,11 @@ const TeamInfoView: React.FC = ({ }; if (loading) { - return
Loading...
; + return
{t("common.loading")}
; } if (!teamData?.team_info) { - return
Team not found
; + return
{t("teamPage.teamInfo.teamNotFound")}
; } const { team_info: info } = teamData; @@ -708,7 +705,7 @@ const TeamInfoView: React.FC = ({
{info.team_alias}
@@ -734,32 +731,53 @@ const TeamInfoView: React.FC = ({ items={[ { key: TEAM_INFO_TAB_KEYS.OVERVIEW, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.OVERVIEW], + label: t("teamPage.teamInfo.tabOverview"), children: ( - Budget Status + {t("teamPage.teamInfo.budgetStatus")}
${formatNumberWithCommas(info.spend, 4)} - of {info.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(info.max_budget, 4)}`} + {t("teamPage.teamInfo.budgetOf", { + value: + info.max_budget === null + ? t("teamPage.teamInfo.unlimited") + : `$${formatNumberWithCommas(info.max_budget, 4)}`, + })} - {info.budget_duration && Reset: {info.budget_duration}} + {info.budget_duration && ( + + {t("teamPage.teamInfo.budgetReset", { duration: info.budget_duration })} + + )}
{info.team_member_budget_table && ( - Team Member Budget: ${formatNumberWithCommas(info.team_member_budget_table.max_budget, 4)} + {t("teamPage.teamInfo.teamMemberBudget", { + amount: formatNumberWithCommas(info.team_member_budget_table.max_budget, 4), + })} )}
- Rate Limits + {t("teamPage.teamInfo.rateLimits")}
- TPM: {info.tpm_limit || "Unlimited"} - RPM: {info.rpm_limit || "Unlimited"} - {info.max_parallel_requests && Max Parallel Requests: {info.max_parallel_requests}} + + {t("teamPage.teamInfo.tpmLabel", { + value: info.tpm_limit || t("teamPage.teamInfo.unlimited"), + })} + + + {t("teamPage.teamInfo.rpmLabel", { + value: info.rpm_limit || t("teamPage.teamInfo.unlimited"), + })} + + {info.max_parallel_requests && ( + {t("teamPage.teamInfo.maxParallelRequests", { value: info.max_parallel_requests })} + )} {(() => { const modelTpm = (info.metadata?.model_tpm_limit ?? {}) as Record; const modelRpm = (info.metadata?.model_rpm_limit ?? {}) as Record; @@ -767,10 +785,14 @@ const TeamInfoView: React.FC = ({ if (models.length === 0) return null; return (
- Per-model limits: + {t("teamPage.teamInfo.perModelLimits")} {models.map((m) => ( - {m}: TPM {modelTpm[m] ?? "—"}, RPM {modelRpm[m] ?? "—"} + {t("teamPage.teamInfo.perModelEntry", { + model: m, + tpm: modelTpm[m] ?? "—", + rpm: modelRpm[m] ?? "—", + })} ))}
@@ -780,10 +802,10 @@ const TeamInfoView: React.FC = ({ - Models + {t("teamPage.teamInfo.modelsLabel")}
{info.models.length === 0 || info.models.includes("all-proxy-models") ? ( - All proxy models + {t("teamPage.teamInfo.allProxyModels")} ) : ( <> {info.models.map((model: string, index: number) => ( @@ -792,7 +814,7 @@ const TeamInfoView: React.FC = ({ ))} {(info.access_group_models || []).map((model: string, index: number) => ( - + {model} ))} @@ -802,11 +824,19 @@ const TeamInfoView: React.FC = ({ - Virtual Keys + {t("teamPage.teamInfo.virtualKeys")}
- User Keys: {teamData.keys.filter((key) => key.user_id).length} - Service Account Keys: {teamData.keys.filter((key) => !key.user_id).length} - Total: {teamData.keys.length} + + {t("teamPage.teamInfo.userKeys", { count: teamData.keys.filter((key) => key.user_id).length })} + + + {t("teamPage.teamInfo.serviceAccountKeys", { + count: teamData.keys.filter((key) => !key.user_id).length, + })} + + + {t("teamPage.teamInfo.totalKeys", { count: teamData.keys.length })} +
@@ -831,18 +861,22 @@ const TeamInfoView: React.FC = ({ - Policies + {t("teamPage.teamInfo.policiesLabel")} {info.policies && info.policies.length > 0 ? (
{info.policies.map((policy: string, index: number) => (
{policy} - {loadingPolicies && Loading guardrails...} + {loadingPolicies && ( + {t("teamPage.teamInfo.loadingGuardrails")} + )}
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
- Resolved Guardrails: + + {t("teamPage.teamInfo.resolvedGuardrails")} +
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => ( @@ -856,7 +890,7 @@ const TeamInfoView: React.FC = ({ ))}
) : ( - No policies configured + {t("teamPage.teamInfo.noPoliciesConfigured")} )} @@ -870,17 +904,17 @@ const TeamInfoView: React.FC = ({ }, { key: TEAM_INFO_TAB_KEYS.MY_USER, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MY_USER], + label: t("teamPage.teamInfo.tabMyUser"), children: , }, { key: TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS], + label: t("teamPage.teamInfo.tabVirtualKeys"), children: , }, { key: TEAM_INFO_TAB_KEYS.MEMBERS, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS], + label: t("teamPage.teamInfo.tabMembers"), children: ( = ({ }, { key: TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS], + label: t("teamPage.teamInfo.tabMemberPermissions"), children: , }, { key: TEAM_INFO_TAB_KEYS.SETTINGS, - label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.SETTINGS], + label: t("teamPage.teamInfo.tabSettings"), children: (
- Team Settings + {t("teamPage.teamInfo.teamSettings")} {canEditTeam && !isEditing && ( )}
{isEditing && isGuardrailsLoading ? ( -
Loading...
+
{t("common.loading")}
) : isEditing ? (
= ({ layout="vertical" > = ({ /> - + - + - + - Team Member Settings + {t("teamPage.teamInfo.teamMemberSettings")} - Optional defaults applied when members join this team. All fields can be overridden per - member. + {t("teamPage.teamInfo.teamMemberSettingsDesc")} - Default Model Access{" "} - + {t("teamPage.teamInfo.defaultModelAccessLabel")}{" "} + @@ -1068,7 +1101,7 @@ const TeamInfoView: React.FC = ({ return ( - daily - weekly - monthly + {t("teamPage.teamInfo.daily")} + {t("teamPage.teamInfo.weekly")} + {t("teamPage.teamInfo.monthly")} - + - + {(fields, { add, remove }) => ( @@ -1143,14 +1187,14 @@ const TeamInfoView: React.FC = ({ {...restField} name={[name, "model"]} rules={[ - { required: true, message: "Missing model" }, + { required: true, message: t("teamPage.teamInfo.missingModel") }, { validator: (_, value) => { if (!value) return Promise.resolve(); const all = form.getFieldValue("modelLimits") ?? []; const dupes = all.filter((entry: { model?: string }) => entry?.model === value); if (dupes.length > 1) { - return Promise.reject(new Error("Duplicate model")); + return Promise.reject(new Error(t("teamPage.teamInfo.duplicateModel"))); } return Promise.resolve(); }, @@ -1160,7 +1204,7 @@ const TeamInfoView: React.FC = ({ > @@ -1237,7 +1283,7 @@ const TeamInfoView: React.FC = ({ label={ <> - Global + {t("teamPage.teamInfo.globalGuardrailsGroup")} } > @@ -1254,7 +1300,7 @@ const TeamInfoView: React.FC = ({ ))} - + {(guardrailsData?.guardrails ?? []) .filter((g) => !g.litellm_params?.default_on) .map((g) => ( @@ -1269,8 +1315,8 @@ const TeamInfoView: React.FC = ({ - Disable all global guardrails{" "} - + {t("teamPage.teamInfo.disableGlobalGuardrailsLabel")}{" "} + @@ -1278,14 +1324,14 @@ const TeamInfoView: React.FC = ({ name="disable_global_guardrails" valuePropName="checked" > - + - Policies{" "} - + {t("teamPage.teamInfo.policiesLabel")}{" "} + = ({ > ({ @@ -1425,7 +1475,7 @@ const TeamInfoView: React.FC = ({ /> - + form.setFieldValue("logging_settings", values)} @@ -1433,12 +1483,12 @@ const TeamInfoView: React.FC = ({ = ({ JSON.parse(value); return Promise.resolve(); } catch (error) { - return Promise.reject(new Error("Please enter valid JSON")); + return Promise.reject(new Error(t("teamPage.teamInfo.secretManagerSettingsJsonError"))); } }, }, @@ -1463,14 +1513,14 @@ const TeamInfoView: React.FC = ({ /> - +
@@ -1486,19 +1536,19 @@ const TeamInfoView: React.FC = ({ ) : (
- Team Name + {t("teamPage.teamInfo.teamNameLabel")}
{info.team_alias}
- Team ID + {t("teamPage.teamInfo.teamIdLabel")}
{info.team_id}
- Created At + {t("common.createdAt")}
{new Date(info.created_at).toLocaleString()}
- Models + {t("teamPage.teamInfo.modelsLabel")}
{info.models.map((model, index) => ( @@ -1509,7 +1559,7 @@ const TeamInfoView: React.FC = ({
{info.default_team_member_models && info.default_team_member_models.length > 0 && (
- Default Member Models + {t("teamPage.teamInfo.defaultMemberModels")}
{info.default_team_member_models.map((model, index) => ( @@ -1520,9 +1570,17 @@ const TeamInfoView: React.FC = ({
)}
- Rate Limits -
TPM: {info.tpm_limit || "Unlimited"}
-
RPM: {info.rpm_limit || "Unlimited"}
+ {t("teamPage.teamInfo.rateLimits")} +
+ {t("teamPage.teamInfo.tpmLabel", { + value: info.tpm_limit || t("teamPage.teamInfo.unlimited"), + })} +
+
+ {t("teamPage.teamInfo.rpmLabel", { + value: info.rpm_limit || t("teamPage.teamInfo.unlimited"), + })} +
{(() => { const modelTpm = (info.metadata?.model_tpm_limit ?? {}) as Record; const modelRpm = (info.metadata?.model_rpm_limit ?? {}) as Record; @@ -1530,10 +1588,14 @@ const TeamInfoView: React.FC = ({ if (models.length === 0) return null; return (
- Per-model limits: + {t("teamPage.teamInfo.perModelLimits")} {models.map((m) => (
- {m}: TPM {modelTpm[m] ?? "—"}, RPM {modelRpm[m] ?? "—"} + {t("teamPage.teamInfo.perModelEntry", { + model: m, + tpm: modelTpm[m] ?? "—", + rpm: modelRpm[m] ?? "—", + })}
))}
@@ -1541,39 +1603,73 @@ const TeamInfoView: React.FC = ({ })()}
- Team Budget + {t("teamPage.teamInfo.teamBudgetLabel")}
- Max Budget:{" "} - {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : "No Limit"} + {t("teamPage.teamInfo.maxBudgetDisplay", { + value: + info.max_budget !== null + ? `$${formatNumberWithCommas(info.max_budget, 4)}` + : t("teamPage.teamInfo.noLimit"), + })}
- Soft Budget:{" "} - {info.soft_budget !== null && info.soft_budget !== undefined - ? `$${formatNumberWithCommas(info.soft_budget, 4)}` - : "No Limit"} + {t("teamPage.teamInfo.softBudgetDisplay", { + value: + info.soft_budget !== null && info.soft_budget !== undefined + ? `$${formatNumberWithCommas(info.soft_budget, 4)}` + : t("teamPage.teamInfo.noLimit"), + })} +
+
+ {t("teamPage.teamInfo.budgetResetDisplay", { + value: info.budget_duration || t("common.never"), + })}
-
Budget Reset: {info.budget_duration || "Never"}
{info.metadata?.soft_budget_alerting_emails && Array.isArray(info.metadata.soft_budget_alerting_emails) && info.metadata.soft_budget_alerting_emails.length > 0 && ( -
Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}
+
+ {t("teamPage.teamInfo.softBudgetEmailsDisplay", { + emails: info.metadata.soft_budget_alerting_emails.join(", "), + })} +
)}
- Team Member Settings{" "} - + {t("teamPage.teamInfo.teamMemberSettings")}{" "} + -
Max Budget: {info.team_member_budget_table?.max_budget || "No Limit"}
-
Budget Duration: {info.team_member_budget_table?.budget_duration || "No Limit"}
-
Key Duration: {info.metadata?.team_member_key_duration || "No Limit"}
-
TPM Limit: {info.team_member_budget_table?.tpm_limit || "No Limit"}
-
RPM Limit: {info.team_member_budget_table?.rpm_limit || "No Limit"}
+
+ {t("teamPage.teamInfo.maxBudgetEntry", { + value: info.team_member_budget_table?.max_budget || t("teamPage.teamInfo.noLimit"), + })} +
+
+ {t("teamPage.teamInfo.budgetDurationEntry", { + value: info.team_member_budget_table?.budget_duration || t("teamPage.teamInfo.noLimit"), + })} +
+
+ {t("teamPage.teamInfo.keyDurationEntry", { + value: info.metadata?.team_member_key_duration || t("teamPage.teamInfo.noLimit"), + })} +
+
+ {t("teamPage.teamInfo.tpmLimitEntry", { + value: info.team_member_budget_table?.tpm_limit || t("teamPage.teamInfo.noLimit"), + })} +
+
+ {t("teamPage.teamInfo.rpmLimitEntry", { + value: info.team_member_budget_table?.rpm_limit || t("teamPage.teamInfo.noLimit"), + })} +
- Router Settings + {t("teamPage.teamInfo.routerSettingsLabel")} {info.router_settings && Object.values(info.router_settings).some( (v) => v !== null && v !== undefined && v !== "" && !(Array.isArray(v) && v.length === 0), @@ -1581,40 +1677,55 @@ const TeamInfoView: React.FC = ({
{info.router_settings.routing_strategy && (
- Routing Strategy: {info.router_settings.routing_strategy} + {t("teamPage.teamInfo.routingStrategy")}{" "} + {info.router_settings.routing_strategy}
)} {info.router_settings.num_retries != null && ( -
Number of Retries: {info.router_settings.num_retries}
+
{t("teamPage.teamInfo.numRetries", { value: info.router_settings.num_retries })}
)} {info.router_settings.allowed_fails != null && ( -
Allowed Failures: {info.router_settings.allowed_fails}
+
+ {t("teamPage.teamInfo.allowedFailures", { value: info.router_settings.allowed_fails })} +
)} {info.router_settings.cooldown_time != null && ( -
Cooldown Time: {info.router_settings.cooldown_time}s
+
+ {t("teamPage.teamInfo.cooldownTime", { value: info.router_settings.cooldown_time })} +
+ )} + {info.router_settings.timeout != null && ( +
{t("teamPage.teamInfo.timeout", { value: info.router_settings.timeout })}
)} - {info.router_settings.timeout != null &&
Timeout: {info.router_settings.timeout}s
} {info.router_settings.retry_after != null && ( -
Retry After: {info.router_settings.retry_after}s
+
{t("teamPage.teamInfo.retryAfter", { value: info.router_settings.retry_after })}
)} {info.router_settings.fallbacks && Array.isArray(info.router_settings.fallbacks) && info.router_settings.fallbacks.length > 0 && ( -
Fallbacks: {info.router_settings.fallbacks.length} configured
+
+ {t("teamPage.teamInfo.fallbacksCount", { + count: info.router_settings.fallbacks.length, + })} +
)} - {info.router_settings.enable_tag_filtering &&
Tag Filtering: Enabled
} + {info.router_settings.enable_tag_filtering && ( +
{t("teamPage.teamInfo.tagFilteringEnabled")}
+ )}
) : ( -
No router settings configured
+
{t("teamPage.teamInfo.noRouterSettings")}
)}
- Organization ID + {t("teamPage.teamInfo.organizationIdLabel")}
{info.organization_id}
- Status - {info.blocked ? "Blocked" : "Active"} + {t("common.status")} + + {info.blocked ? t("teamPage.teamInfo.blockedStatus") : t("teamPage.teamInfo.activeStatus")} +
= ({ {info.metadata?.secret_manager_settings && (
- Secret Manager Settings + {t("teamPage.teamInfo.secretManagerSettingsLabel")}
                           {JSON.stringify(info.metadata.secret_manager_settings, null, 2)}
                         
@@ -1667,20 +1778,20 @@ const TeamInfoView: React.FC = ({ initialData={selectedEditMember} mode="edit" config={{ - title: "Edit Member", + title: t("teamPage.teamInfo.editMemberTitle"), showEmail: true, showUserId: true, roleOptions: [ - { label: "Admin", value: "admin" }, - { label: "User", value: "user" }, + { label: t("teamPage.teamInfo.adminRole"), value: "admin" }, + { label: t("teamPage.teamInfo.userRole"), value: "user" }, ], additionalFields: [ { name: "max_budget_in_team", label: ( - Team Member Budget (USD){" "} - + {t("teamPage.teamInfo.teamMemberBudgetLabel")}{" "} + @@ -1688,14 +1799,14 @@ const TeamInfoView: React.FC = ({ type: "numerical" as const, step: 0.01, min: 0, - placeholder: "Budget limit for this member within this team", + placeholder: t("teamPage.teamInfo.teamMemberBudgetPlaceholder"), }, { name: "budget_duration", label: ( - Budget Reset Period{" "} - + {t("teamPage.teamInfo.budgetResetPeriodLabel")}{" "} + @@ -1706,8 +1817,8 @@ const TeamInfoView: React.FC = ({ name: "tpm_limit", label: ( - Team Member TPM Limit{" "} - + {t("teamPage.teamInfo.teamMemberTpmLabel")}{" "} + @@ -1715,14 +1826,14 @@ const TeamInfoView: React.FC = ({ type: "numerical" as const, step: 1, min: 0, - placeholder: "Tokens per minute limit for this member in this team", + placeholder: t("teamPage.teamInfo.teamMemberTpmPlaceholder"), }, { name: "rpm_limit", label: ( - Team Member RPM Limit{" "} - + {t("teamPage.teamInfo.teamMemberRpmLabel")}{" "} + @@ -1730,21 +1841,21 @@ const TeamInfoView: React.FC = ({ type: "numerical" as const, step: 1, min: 0, - placeholder: "Requests per minute limit for this member in this team", + placeholder: t("teamPage.teamInfo.teamMemberRpmPlaceholder"), }, { name: "allowed_models", label: ( - Allowed Models{" "} - + {t("teamPage.teamInfo.allowedModelsLabel")}{" "} + ), type: "multi-select" as const, options: (info.models || []).map((m: string) => ({ label: m, value: m })), - placeholder: "Leave empty to inherit all team models", + placeholder: t("teamPage.teamInfo.allowedModelsPlaceholder"), }, ], }} @@ -1761,14 +1872,14 @@ const TeamInfoView: React.FC = ({ {/* Delete Member Confirmation Modal */} { - describe("TEAM_INFO_TAB_LABELS", () => { - it("should have label for every tab key", () => { - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.OVERVIEW]).toBe("Overview"); - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MY_USER]).toBe("My User"); - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]).toBe("Virtual Keys"); - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS]).toBe("Members"); - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]).toBe("Member Permissions"); - expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.SETTINGS]).toBe("Settings"); - }); - }); - describe("getTeamInfoVisibleTabs", () => { it("returns overview, my user, and virtual keys when user cannot edit team", () => { const tabs = getTeamInfoVisibleTabs(false); diff --git a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts index 2493c418566..c9b289ae54b 100644 --- a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts +++ b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts @@ -12,15 +12,6 @@ export const TEAM_INFO_TAB_KEYS = { SETTINGS: "settings", } as const; -export const TEAM_INFO_TAB_LABELS: Record = { - [TEAM_INFO_TAB_KEYS.OVERVIEW]: "Overview", - [TEAM_INFO_TAB_KEYS.MY_USER]: "My User", - [TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS]: "Virtual Keys", - [TEAM_INFO_TAB_KEYS.MEMBERS]: "Members", - [TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]: "Member Permissions", - [TEAM_INFO_TAB_KEYS.SETTINGS]: "Settings", -}; - /** * Returns the list of tab keys that should be visible based on permissions. * - Overview, My User, Virtual Keys: always visible diff --git a/ui/litellm-dashboard/src/locales/en.json b/ui/litellm-dashboard/src/locales/en.json index b3ca4343f37..4aceafb8625 100644 --- a/ui/litellm-dashboard/src/locales/en.json +++ b/ui/litellm-dashboard/src/locales/en.json @@ -6247,6 +6247,177 @@ "modelScopeLabel": "Model Scope", "modelScopeTooltip": "Models you can access within this team.", "allTeamModels": "All Team Models" + }, + "teamInfo": { + "failedToLoadTeam": "Failed to load team information", + "memberAddedSuccess": "Team member added successfully", + "assignAdminsEnterprise": "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.", + "failedToAddMember": "Failed to add team member", + "memberUpdatedSuccess": "Team member updated successfully", + "failedToUpdateMember": "Failed to update team member", + "memberRemovedSuccess": "Team member removed successfully", + "failedToRemoveMember": "Failed to remove team member", + "invalidMetadataJson": "Invalid JSON in metadata field", + "invalidSecretManagerJson": "Invalid JSON in secret manager settings", + "teamSettingsUpdatedSuccess": "Team settings updated successfully", + "teamNotFound": "Team not found", + "backToTeams": "Back to Teams", + "tabOverview": "Overview", + "tabMyUser": "My User", + "tabVirtualKeys": "Virtual Keys", + "tabMembers": "Members", + "tabMemberPermissions": "Member Permissions", + "tabSettings": "Settings", + "budgetStatus": "Budget Status", + "unlimited": "Unlimited", + "budgetOf": "of {{value}}", + "budgetReset": "Reset: {{duration}}", + "teamMemberBudget": "Team Member Budget: ${{amount}}", + "rateLimits": "Rate Limits", + "tpmLabel": "TPM: {{value}}", + "rpmLabel": "RPM: {{value}}", + "maxParallelRequests": "Max Parallel Requests: {{value}}", + "perModelLimits": "Per-model limits:", + "perModelEntry": "{{model}}: TPM {{tpm}}, RPM {{rpm}}", + "modelsLabel": "Models", + "allProxyModels": "All proxy models", + "fromAccessGroup": "From access group", + "virtualKeys": "Virtual Keys", + "userKeys": "User Keys: {{count}}", + "serviceAccountKeys": "Service Account Keys: {{count}}", + "totalKeys": "Total: {{count}}", + "loadingGuardrails": "Loading guardrails...", + "resolvedGuardrails": "Resolved Guardrails:", + "noPoliciesConfigured": "No policies configured", + "policiesLabel": "Policies", + "teamSettings": "Team Settings", + "editSettings": "Edit Settings", + "teamNameLabel": "Team Name", + "teamNameRequired": "Please input a team name", + "modelsRequired": "Please select at least one model", + "maxBudgetLabel": "Max Budget (USD)", + "softBudgetLabel": "Soft Budget (USD)", + "softBudgetEmailsLabel": "Soft Budget Alerting Emails", + "softBudgetEmailsTooltip": "Comma-separated email addresses to receive alerts when the soft budget is reached", + "softBudgetEmailsPlaceholder": "example1@test.com, example2@test.com", + "teamMemberSettings": "Team Member Settings", + "teamMemberSettingsDesc": "Optional defaults applied when members join this team. All fields can be overridden per member.", + "teamMemberSettingsTooltip": "These are limits on individual team members", + "defaultModelAccessLabel": "Default Model Access", + "defaultModelAccessTooltip": "Optional. If set, new members can only access these models by default. Must be a subset of the team's models above. Leave empty to give all members access to all team models.", + "defaultModelAccessPlaceholder": "Leave empty — all team models accessible to every member", + "defaultBudgetLabel": "Default Budget (USD)", + "defaultBudgetTooltip": "Default spend budget for each member in this team.", + "defaultBudgetDurationLabel": "Default Budget Duration", + "defaultKeyDurationLabel": "Default Key Duration (eg: 1d, 1mo)", + "defaultKeyDurationTooltip": "Set a limit to the duration of a team member's key. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days), 1mo (month)", + "defaultKeyDurationPlaceholder": "e.g., 30d", + "defaultTpmLimitLabel": "Default TPM Limit", + "defaultTpmLimitTooltip": "Default tokens per minute limit for each member. Can be overridden per member.", + "defaultTpmLimitPlaceholder": "e.g., 1000", + "defaultRpmLimitLabel": "Default RPM Limit", + "defaultRpmLimitTooltip": "Default requests per minute limit for each member. Can be overridden per member.", + "defaultRpmLimitPlaceholder": "e.g., 100", + "resetBudgetLabel": "Reset Budget", + "daily": "daily", + "weekly": "weekly", + "monthly": "monthly", + "tpmLimitLabel": "Tokens per minute Limit (TPM)", + "rpmLimitLabel": "Requests per minute Limit (RPM)", + "modelSpecificRateLimitsLabel": "Model-Specific Rate Limits", + "modelSpecificRateLimitsTooltip": "Set per-model TPM/RPM limits that apply across the whole team.", + "missingModel": "Missing model", + "duplicateModel": "Duplicate model", + "setAtLeastOneLimitError": "Set at least one of TPM or RPM", + "selectModelPlaceholder": "Select model", + "tpmLimitPlaceholder": "TPM Limit", + "rpmLimitPlaceholder": "RPM Limit", + "addModelLimitButton": "Add Model Limit", + "routerSettingsLabel": "Router Settings", + "guardrailsLabel": "Guardrails", + "guardrailsTooltip": "Select which guardrails apply to this team. Global guardrails are enabled by default — uncheck to opt out. Other guardrails are opt-in.", + "selectGuardrailsPlaceholder": "Select guardrails", + "globalGuardrailsGroup": "Global", + "otherGuardrailsGroup": "Other", + "disableGlobalGuardrailsLabel": "Disable all global guardrails", + "disableGlobalGuardrailsTooltip": "Kill switch: bypass every global guardrail for this team, including any added in the future. For per-guardrail opt-out instead, use the Guardrails dropdown above.", + "policiesTooltip": "Apply policies to this team to control guardrails and other settings", + "selectPoliciesPlaceholder": "Select or enter policies", + "accessGroupsLabel": "Access Groups", + "accessGroupsTooltip": "Assign access groups to this team. Access groups control which models, MCP servers, and agents this team can use", + "accessGroupsPlaceholder": "Select access groups (optional)", + "vectorStoresLabel": "Vector Stores", + "vectorStoresPlaceholder": "Select vector stores", + "allowedPassThroughLabel": "Allowed Pass Through Routes", + "passThroughPremiumTooltip": "Premium feature - Upgrade to set allowed pass through routes", + "passThroughAdminTooltip": "Only proxy admins can set allowed pass through routes", + "passThroughPlaceholder": "Select pass through routes", + "mcpServersLabel": "MCP Servers / Access Groups", + "mcpServersPlaceholder": "Select MCP servers or access groups (optional)", + "agentsLabel": "Agents / Access Groups", + "agentsPlaceholder": "Select agents or access groups (optional)", + "searchToolSettingsHeader": "Search Tool Settings", + "allowedSearchToolsLabel": "Allowed Search Tools", + "allowedSearchToolsTooltip": "Select which search tools this team can access. Leave empty to allow all search tools.", + "searchToolsPlaceholder": "Select search tools (optional, empty = all allowed)", + "organizationLabel": "Organization", + "organizationPlaceholder": "Select an organization", + "loggingSettingsLabel": "Logging Settings", + "secretManagerSettingsLabel": "Secret Manager Settings", + "secretManagerSettingsHelp": "Enter secret manager configuration as a JSON object.", + "secretManagerSettingsPremiumHelp": "Premium feature - Upgrade to manage secret manager settings.", + "secretManagerSettingsJsonError": "Please enter valid JSON", + "metadataLabel": "Metadata", + "saveChanges": "Save Changes", + "teamIdLabel": "Team ID", + "defaultMemberModels": "Default Member Models", + "teamBudgetLabel": "Team Budget", + "maxBudgetDisplay": "Max Budget: {{value}}", + "softBudgetDisplay": "Soft Budget: {{value}}", + "noLimit": "No Limit", + "budgetResetDisplay": "Budget Reset: {{value}}", + "softBudgetEmailsDisplay": "Soft Budget Alerting Emails: {{emails}}", + "maxBudgetEntry": "Max Budget: {{value}}", + "budgetDurationEntry": "Budget Duration: {{value}}", + "keyDurationEntry": "Key Duration: {{value}}", + "tpmLimitEntry": "TPM Limit: {{value}}", + "rpmLimitEntry": "RPM Limit: {{value}}", + "routingStrategy": "Routing Strategy:", + "numRetries": "Number of Retries: {{value}}", + "allowedFailures": "Allowed Failures: {{value}}", + "cooldownTime": "Cooldown Time: {{value}}s", + "timeout": "Timeout: {{value}}s", + "retryAfter": "Retry After: {{value}}s", + "fallbacksCount": "Fallbacks: {{count}} configured", + "tagFilteringEnabled": "Tag Filtering: Enabled", + "noRouterSettings": "No router settings configured", + "organizationIdLabel": "Organization ID", + "blockedStatus": "Blocked", + "activeStatus": "Active", + "editMemberTitle": "Edit Member", + "adminRole": "Admin", + "userRole": "User", + "teamMemberBudgetLabel": "Team Member Budget (USD)", + "teamMemberBudgetTooltip": "Maximum amount in USD this member can spend within this team. This is separate from any global user budget limits", + "teamMemberBudgetPlaceholder": "Budget limit for this member within this team", + "budgetResetPeriodLabel": "Budget Reset Period", + "budgetResetPeriodTooltip": "How often this member's budget resets within the team. Leave unset and the budget never resets.", + "teamMemberTpmLabel": "Team Member TPM Limit", + "teamMemberTpmTooltip": "Maximum tokens per minute this member can use within this team. This is separate from any global user TPM limit", + "teamMemberTpmPlaceholder": "Tokens per minute limit for this member in this team", + "teamMemberRpmLabel": "Team Member RPM Limit", + "teamMemberRpmTooltip": "Maximum requests per minute this member can make within this team. This is separate from any global user RPM limit", + "teamMemberRpmPlaceholder": "Requests per minute limit for this member in this team", + "allowedModelsLabel": "Allowed Models", + "allowedModelsTooltip": "Models this member can access within this team. Leave empty to inherit all team models.", + "allowedModelsPlaceholder": "Leave empty to inherit all team models", + "deleteMemberTitle": "Delete Team Member", + "deleteMemberAlert": "Removing team members will also delete any keys created by or created for this member.", + "deleteMemberMessage": "Are you sure you want to remove this member from the team? This action cannot be undone.", + "deleteMemberInfoTitle": "Team Member Information", + "userIdLabel": "User ID", + "emailLabel": "Email", + "roleLabel": "Role" } } } diff --git a/ui/litellm-dashboard/src/locales/zh-CN.json b/ui/litellm-dashboard/src/locales/zh-CN.json index b3b31c1e8dc..0805be9fb2a 100644 --- a/ui/litellm-dashboard/src/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/locales/zh-CN.json @@ -6247,6 +6247,177 @@ "modelScopeLabel": "模型范围", "modelScopeTooltip": "您在此团队中可访问的模型。", "allTeamModels": "所有团队模型" + }, + "teamInfo": { + "failedToLoadTeam": "加载团队信息失败", + "memberAddedSuccess": "团队成员添加成功", + "assignAdminsEnterprise": "指定管理员是企业专属功能,请升级您的 LiteLLM 套餐以启用此功能。", + "failedToAddMember": "添加团队成员失败", + "memberUpdatedSuccess": "团队成员更新成功", + "failedToUpdateMember": "更新团队成员失败", + "memberRemovedSuccess": "团队成员移除成功", + "failedToRemoveMember": "移除团队成员失败", + "invalidMetadataJson": "元数据字段中的 JSON 无效", + "invalidSecretManagerJson": "密钥管理器设置中的 JSON 无效", + "teamSettingsUpdatedSuccess": "团队设置更新成功", + "teamNotFound": "未找到团队", + "backToTeams": "返回团队列表", + "tabOverview": "概览", + "tabMyUser": "我的用户", + "tabVirtualKeys": "虚拟密钥", + "tabMembers": "成员", + "tabMemberPermissions": "成员权限", + "tabSettings": "设置", + "budgetStatus": "预算状态", + "unlimited": "不限", + "budgetOf": "共 {{value}}", + "budgetReset": "重置:{{duration}}", + "teamMemberBudget": "团队成员预算:${{amount}}", + "rateLimits": "速率限制", + "tpmLabel": "TPM:{{value}}", + "rpmLabel": "RPM:{{value}}", + "maxParallelRequests": "最大并发请求数:{{value}}", + "perModelLimits": "按模型限制:", + "perModelEntry": "{{model}}:TPM {{tpm}},RPM {{rpm}}", + "modelsLabel": "模型", + "allProxyModels": "所有代理模型", + "fromAccessGroup": "来自访问组", + "virtualKeys": "虚拟密钥", + "userKeys": "用户密钥:{{count}}", + "serviceAccountKeys": "服务账号密钥:{{count}}", + "totalKeys": "共计:{{count}}", + "loadingGuardrails": "正在加载护栏...", + "resolvedGuardrails": "已解析护栏:", + "noPoliciesConfigured": "未配置策略", + "policiesLabel": "策略", + "teamSettings": "团队设置", + "editSettings": "编辑设置", + "teamNameLabel": "团队名称", + "teamNameRequired": "请输入团队名称", + "modelsRequired": "请至少选择一个模型", + "maxBudgetLabel": "最大预算(USD)", + "softBudgetLabel": "软预算(USD)", + "softBudgetEmailsLabel": "软预算告警邮箱", + "softBudgetEmailsTooltip": "达到软预算时接收告警的邮箱地址,多个地址以逗号分隔", + "softBudgetEmailsPlaceholder": "example1@test.com, example2@test.com", + "teamMemberSettings": "团队成员设置", + "teamMemberSettingsDesc": "成员加入此团队时应用的可选默认值,所有字段均可按成员单独覆盖。", + "teamMemberSettingsTooltip": "这些是对单个团队成员的限制", + "defaultModelAccessLabel": "默认模型访问权限", + "defaultModelAccessTooltip": "可选。设置后,新成员默认只能访问这些模型,必须是上方团队模型的子集。留空则所有成员可访问全部团队模型。", + "defaultModelAccessPlaceholder": "留空 — 所有成员均可访问全部团队模型", + "defaultBudgetLabel": "默认预算(USD)", + "defaultBudgetTooltip": "此团队中每位成员的默认花费预算。", + "defaultBudgetDurationLabel": "默认预算周期", + "defaultKeyDurationLabel": "默认密钥有效期(如:1d、1mo)", + "defaultKeyDurationTooltip": "设置团队成员密钥的有效期上限。格式:30s(秒)、30m(分钟)、30h(小时)、30d(天)、1mo(月)", + "defaultKeyDurationPlaceholder": "如:30d", + "defaultTpmLimitLabel": "默认 TPM 限制", + "defaultTpmLimitTooltip": "每位成员的默认每分钟 Token 限制,可按成员单独覆盖。", + "defaultTpmLimitPlaceholder": "如:1000", + "defaultRpmLimitLabel": "默认 RPM 限制", + "defaultRpmLimitTooltip": "每位成员的默认每分钟请求限制,可按成员单独覆盖。", + "defaultRpmLimitPlaceholder": "如:100", + "resetBudgetLabel": "重置预算", + "daily": "每日", + "weekly": "每周", + "monthly": "每月", + "tpmLimitLabel": "每分钟 Token 限制(TPM)", + "rpmLimitLabel": "每分钟请求限制(RPM)", + "modelSpecificRateLimitsLabel": "按模型速率限制", + "modelSpecificRateLimitsTooltip": "设置适用于整个团队的按模型 TPM/RPM 限制。", + "missingModel": "缺少模型", + "duplicateModel": "重复模型", + "setAtLeastOneLimitError": "请至少设置 TPM 或 RPM 中的一个", + "selectModelPlaceholder": "选择模型", + "tpmLimitPlaceholder": "TPM 限制", + "rpmLimitPlaceholder": "RPM 限制", + "addModelLimitButton": "添加模型限制", + "routerSettingsLabel": "路由器设置", + "guardrailsLabel": "护栏", + "guardrailsTooltip": "选择适用于此团队的护栏。全局护栏默认启用,取消勾选可退出。其他护栏需手动选入。", + "selectGuardrailsPlaceholder": "选择护栏", + "globalGuardrailsGroup": "全局", + "otherGuardrailsGroup": "其他", + "disableGlobalGuardrailsLabel": "禁用所有全局护栏", + "disableGlobalGuardrailsTooltip": "终止开关:绕过此团队的所有全局护栏,包括未来新增的护栏。如需按护栏单独退出,请使用上方的护栏下拉菜单。", + "policiesTooltip": "为此团队应用策略以控制护栏和其他设置", + "selectPoliciesPlaceholder": "选择或输入策略", + "accessGroupsLabel": "访问组", + "accessGroupsTooltip": "为此团队分配访问组,访问组控制该团队可使用的模型、MCP 服务器和 Agent", + "accessGroupsPlaceholder": "选择访问组(可选)", + "vectorStoresLabel": "向量存储", + "vectorStoresPlaceholder": "选择向量存储", + "allowedPassThroughLabel": "允许的透传路由", + "passThroughPremiumTooltip": "高级功能 - 请升级以设置允许的透传路由", + "passThroughAdminTooltip": "只有代理管理员可以设置允许的透传路由", + "passThroughPlaceholder": "选择透传路由", + "mcpServersLabel": "MCP 服务器 / 访问组", + "mcpServersPlaceholder": "选择 MCP 服务器或访问组(可选)", + "agentsLabel": "Agent / 访问组", + "agentsPlaceholder": "选择 Agent 或访问组(可选)", + "searchToolSettingsHeader": "搜索工具设置", + "allowedSearchToolsLabel": "允许的搜索工具", + "allowedSearchToolsTooltip": "选择此团队可访问的搜索工具,留空则允许所有搜索工具。", + "searchToolsPlaceholder": "选择搜索工具(可选,留空 = 全部允许)", + "organizationLabel": "组织", + "organizationPlaceholder": "选择组织", + "loggingSettingsLabel": "日志设置", + "secretManagerSettingsLabel": "密钥管理器设置", + "secretManagerSettingsHelp": "以 JSON 对象格式输入密钥管理器配置。", + "secretManagerSettingsPremiumHelp": "高级功能 - 请升级以管理密钥管理器设置。", + "secretManagerSettingsJsonError": "请输入有效的 JSON", + "metadataLabel": "元数据", + "saveChanges": "保存更改", + "teamIdLabel": "团队 ID", + "defaultMemberModels": "默认成员模型", + "teamBudgetLabel": "团队预算", + "maxBudgetDisplay": "最大预算:{{value}}", + "softBudgetDisplay": "软预算:{{value}}", + "noLimit": "无限制", + "budgetResetDisplay": "预算重置:{{value}}", + "softBudgetEmailsDisplay": "软预算告警邮箱:{{emails}}", + "maxBudgetEntry": "最大预算:{{value}}", + "budgetDurationEntry": "预算周期:{{value}}", + "keyDurationEntry": "密钥有效期:{{value}}", + "tpmLimitEntry": "TPM 限制:{{value}}", + "rpmLimitEntry": "RPM 限制:{{value}}", + "routingStrategy": "路由策略:", + "numRetries": "重试次数:{{value}}", + "allowedFailures": "允许失败次数:{{value}}", + "cooldownTime": "冷却时间:{{value}}s", + "timeout": "超时:{{value}}s", + "retryAfter": "重试延迟:{{value}}s", + "fallbacksCount": "回退:已配置 {{count}} 个", + "tagFilteringEnabled": "标签过滤:已启用", + "noRouterSettings": "未配置路由器设置", + "organizationIdLabel": "组织 ID", + "blockedStatus": "已封禁", + "activeStatus": "活跃", + "editMemberTitle": "编辑成员", + "adminRole": "管理员", + "userRole": "用户", + "teamMemberBudgetLabel": "团队成员预算(USD)", + "teamMemberBudgetTooltip": "此成员在该团队内可花费的最大 USD 金额,独立于任何全局用户预算限制", + "teamMemberBudgetPlaceholder": "此成员在该团队内的预算上限", + "budgetResetPeriodLabel": "预算重置周期", + "budgetResetPeriodTooltip": "此成员在团队内的预算重置频率,留空则预算永不重置。", + "teamMemberTpmLabel": "团队成员 TPM 限制", + "teamMemberTpmTooltip": "此成员在该团队内可使用的每分钟最大 Token 数,独立于任何全局用户 TPM 限制", + "teamMemberTpmPlaceholder": "此成员在该团队内的每分钟 Token 限制", + "teamMemberRpmLabel": "团队成员 RPM 限制", + "teamMemberRpmTooltip": "此成员在该团队内可发起的每分钟最大请求数,独立于任何全局用户 RPM 限制", + "teamMemberRpmPlaceholder": "此成员在该团队内的每分钟请求限制", + "allowedModelsLabel": "允许的模型", + "allowedModelsTooltip": "此成员在该团队内可访问的模型,留空则继承所有团队模型。", + "allowedModelsPlaceholder": "留空则继承所有团队模型", + "deleteMemberTitle": "删除团队成员", + "deleteMemberAlert": "移除团队成员将同时删除该成员创建的或为其创建的所有密钥。", + "deleteMemberMessage": "确定要将此成员从团队中移除吗?此操作无法撤销。", + "deleteMemberInfoTitle": "团队成员信息", + "userIdLabel": "用户 ID", + "emailLabel": "邮箱", + "roleLabel": "角色" } } }