feat(ui): i18n for top-level components batch 1

This commit is contained in:
吹雪 2026-06-10 12:21:22 +09:00
parent bd289dc5e8
commit 867abf24f9
8 changed files with 543 additions and 160 deletions

View file

@ -16,6 +16,7 @@ import {
} from "@tremor/react";
import { Alert, Button as Button2, Form, Input, Modal, Space, Tabs, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import NewBadge from "./common_components/NewBadge";
import { useBaseUrl } from "./constants";
import NotificationsManager from "./molecules/notifications_manager";
@ -35,6 +36,7 @@ interface AdminPanelProps {
}
const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
const { t } = useTranslation();
const { premiumUser, accessToken, userId: userID } = useAuthorized();
const [form] = Form.useForm();
const [isAddSSOModalVisible, setIsAddSSOModalVisible] = useState(false);
@ -48,7 +50,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
const [ssoConfigured, setSsoConfigured] = useState<boolean>(false);
const baseUrl = useBaseUrl();
const all_ip_address_allowed = "All IP Addresses Allowed";
const all_ip_address_allowed = t("adminPanel.allIPAddressesAllowed");
let nonSssoUrl = baseUrl;
nonSssoUrl += "/fallback/login";
@ -77,9 +79,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
const handleShowAllowedIPs = async () => {
try {
if (premiumUser !== true) {
NotificationsManager.fromBackend(
"This feature is only available for premium users. Please upgrade your account.",
);
NotificationsManager.fromBackend(t("adminPanel.notifications.premiumOnlyFeature"));
return;
}
if (accessToken) {
@ -90,7 +90,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
}
} catch (error) {
console.error("Error fetching allowed IPs:", error);
NotificationsManager.fromBackend(`Failed to fetch allowed IPs ${error}`);
NotificationsManager.fromBackend(t("adminPanel.fetchAllowedIpsFailed", { error }));
setAllowedIPs([all_ip_address_allowed]);
} finally {
if (premiumUser === true) {
@ -106,11 +106,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
// Fetch the updated list of IPs
const updatedIPs = await getAllowedIPs(accessToken);
setAllowedIPs(updatedIPs);
NotificationsManager.success("IP address added successfully");
NotificationsManager.success(t("adminPanel.notifications.ipAddedSuccess"));
}
} catch (error) {
console.error("Error adding IP:", error);
NotificationsManager.fromBackend(`Failed to add IP address ${error}`);
NotificationsManager.fromBackend(t("adminPanel.addIpFailed", { error }));
} finally {
setIsAddIPModalVisible(false);
}
@ -128,10 +128,10 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
// Fetch the updated list of IPs
const updatedIPs = await getAllowedIPs(accessToken);
setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : [all_ip_address_allowed]);
NotificationsManager.success("IP address deleted successfully");
NotificationsManager.success(t("adminPanel.notifications.ipDeletedSuccess"));
} catch (error) {
console.error("Error deleting IP:", error);
NotificationsManager.fromBackend(`Failed to delete IP address ${error}`);
NotificationsManager.fromBackend(t("adminPanel.deleteIpFailed", { error }));
} finally {
setIsDeleteIPModalVisible(false);
setIPToDelete(null);
@ -186,19 +186,19 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
const tabItems = [
{
key: "sso-settings",
label: "SSO Settings",
label: t("adminPanel.tabs.ssoSettings"),
children: <SSOSettings />,
},
{
key: "security-settings",
label: "Security Settings",
label: t("adminPanel.tabs.securitySettings"),
children: (
<>
<Card>
<Title level={4}> ✨ Security Settings</Title>
<Title level={4}> ✨ {t("adminPanel.securityCard.title")}</Title>
<Alert
message="SSO Configuration Deprecated"
description="Editing SSO Settings on this page is deprecated and will be removed in a future version. Please use the SSO Settings tab for SSO configuration."
message={t("adminPanel.securityCard.deprecatedAlertMessage")}
description={t("adminPanel.securityCard.deprecatedAlertDescription")}
type="warning"
showIcon
/>
@ -213,12 +213,12 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
>
<div>
<Button style={{ width: "150px" }} onClick={() => setIsAddSSOModalVisible(true)}>
{ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
{ssoConfigured ? t("adminPanel.securityCard.editSSOSettings") : t("adminPanel.securityCard.addSSO")}
</Button>
</div>
<div>
<Button style={{ width: "150px" }} onClick={handleShowAllowedIPs}>
Allowed IPs
{t("adminPanel.securityCard.allowedIPs")}
</Button>
</div>
<div>
@ -227,10 +227,10 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
onClick={() =>
premiumUser === true
? setIsUIAccessControlModalVisible(true)
: NotificationsManager.fromBackend("Only premium users can configure UI access control")
: NotificationsManager.fromBackend(t("adminPanel.notifications.premiumOnlyUIAccessControl"))
}
>
UI Access Control
{t("adminPanel.securityCard.uiAccessControl")}
</Button>
</div>
</div>
@ -250,24 +250,26 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
ssoConfigured={ssoConfigured}
/>
<Modal
title="Manage Allowed IP Addresses"
title={t("adminPanel.manageAllowedIPModal.title")}
width={800}
open={isAllowedIPModalVisible}
onCancel={() => setIsAllowedIPModalVisible(false)}
footer={[
<Button className="mx-1" key="add" onClick={() => setIsAddIPModalVisible(true)}>
Add IP Address
{t("adminPanel.manageAllowedIPModal.addIPAddress")}
</Button>,
<Button key="close" onClick={() => setIsAllowedIPModalVisible(false)}>
Close
{t("common.close")}
</Button>,
]}
>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>IP Address</TableHeaderCell>
<TableHeaderCell className="text-right">Action</TableHeaderCell>
<TableHeaderCell>{t("adminPanel.manageAllowedIPModal.ipAddressColumn")}</TableHeaderCell>
<TableHeaderCell className="text-right">
{t("adminPanel.manageAllowedIPModal.actionColumn")}
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -277,7 +279,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
<TableCell className="text-right">
{ip !== all_ip_address_allowed && (
<Button onClick={() => handleDeleteIP(ip)} color="red" size="xs">
Delete
{t("common.delete")}
</Button>
)}
</TableCell>
@ -288,41 +290,41 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
</Modal>
<Modal
title="Add Allowed IP Address"
title={t("adminPanel.addIPModal.title")}
open={isAddIPModalVisible}
onCancel={() => setIsAddIPModalVisible(false)}
footer={null}
>
<Form onFinish={handleAddIP}>
<Form.Item name="ip" rules={[{ required: true, message: "Please enter an IP address" }]}>
<Input placeholder="Enter IP address" />
<Form.Item name="ip" rules={[{ required: true, message: t("adminPanel.addIPModal.inputRequired") }]}>
<Input placeholder={t("adminPanel.addIPModal.inputPlaceholder")} />
</Form.Item>
<Form.Item>
<Button2 htmlType="submit">Add IP Address</Button2>
<Button2 htmlType="submit">{t("adminPanel.addIPModal.submitButton")}</Button2>
</Form.Item>
</Form>
</Modal>
<Modal
title="Confirm Delete"
title={t("adminPanel.deleteIPModal.title")}
open={isDeleteIPModalVisible}
onCancel={() => setIsDeleteIPModalVisible(false)}
onOk={confirmDeleteIP}
footer={[
<Button className="mx-1" key="delete" onClick={() => confirmDeleteIP()}>
Yes
{t("common.yes")}
</Button>,
<Button key="close" onClick={() => setIsDeleteIPModalVisible(false)}>
Close
{t("common.close")}
</Button>,
]}
>
<Text>Are you sure you want to delete the IP address: {ipToDelete}?</Text>
<Text>{t("adminPanel.deleteIPModal.confirmText", { ip: ipToDelete })}</Text>
</Modal>
{/* UI Access Control Modal */}
<Modal
title="UI Access Control Settings"
title={t("adminPanel.uiAccessControlModal.title")}
open={isUIAccessControlModalVisible}
width={600}
footer={null}
@ -333,13 +335,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
accessToken={accessToken}
onSuccess={() => {
handleUIAccessControlOk();
NotificationsManager.success("UI Access Control settings updated successfully");
NotificationsManager.success(t("adminPanel.uiAccessControlModal.updateSuccess"));
}}
/>
</Modal>
</div>
<Callout title="Login without SSO" color="teal">
If you need to login without sso, you can access{" "}
<Callout title={t("adminPanel.loginWithoutSSO.title")} color="teal">
{t("adminPanel.loginWithoutSSO.description")}{" "}
<a href={nonSssoUrl} target="_blank" rel="noopener noreferrer">
<b>{nonSssoUrl}</b>{" "}
</a>
@ -349,7 +351,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
},
{
key: "scim",
label: "SCIM",
label: t("adminPanel.tabs.scim"),
children: <SCIMConfig accessToken={accessToken} userID={userID} proxySettings={proxySettings} />,
},
{
@ -357,7 +359,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
label: (
<Space>
<Text>
UI Settings <NewBadge />
{t("adminPanel.tabs.uiSettings")} <NewBadge />
</Text>
</Space>
),
@ -365,20 +367,20 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
},
{
key: "logging-settings",
label: "Logging Settings",
label: t("adminPanel.tabs.loggingSettings"),
children: <LoggingSettings />,
},
{
key: "hashicorp-vault",
label: "Hashicorp Vault",
label: t("adminPanel.tabs.hashicorpVault"),
children: <HashicorpVault />,
},
];
return (
<div className="w-full m-2 mt-2 p-8">
<Title level={4}>Admin Access </Title>
<Paragraph>Go to &apos;Internal Users&apos; page to add other admins.</Paragraph>
<Title level={4}>{t("adminPanel.title")} </Title>
<Paragraph>{t("adminPanel.goToInternalUsers")}</Paragraph>
<Tabs items={tabItems} />
</div>
);

View file

@ -4,6 +4,7 @@ import { userBulkUpdateUserCall, teamBulkMemberAddCall, Member } from "./network
import { UserEditView } from "./user_edit_view";
import NotificationsManager from "./molecules/notifications_manager";
import MessageManager from "@/components/molecules/message_manager";
import { useTranslation } from "react-i18next";
const { Text, Title } = Typography;
@ -32,6 +33,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
userModels,
allowAllUsers = false,
}) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [selectedTeams, setSelectedTeams] = useState<string[]>([]);
const [teamBudget, setTeamBudget] = useState<number | null>(null);
@ -71,7 +73,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
const handleSubmit = async (formValues: any) => {
console.log("formValues", formValues);
if (!accessToken) {
NotificationsManager.fromBackend("Access token not found");
NotificationsManager.fromBackend(t("bulkEditUsers.notifications.accessTokenNotFound"));
return;
}
@ -107,7 +109,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
const hasTeamAdditions = addToTeams && selectedTeams.length > 0;
if (!hasUserUpdates && !hasTeamAdditions) {
NotificationsManager.fromBackend("Please modify at least one field or select teams to add users to");
NotificationsManager.fromBackend(t("bulkEditUsers.notifications.noFieldsModified"));
return;
}
@ -117,10 +119,10 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
if (hasUserUpdates) {
if (updateAllUsers) {
const result = await userBulkUpdateUserCall(accessToken, updatePayload, undefined, true);
successMessages.push(`Updated all users (${result.total_requested} total)`);
successMessages.push(t("bulkEditUsers.notifications.updatedAllUsers", { total: result.total_requested }));
} else {
await userBulkUpdateUserCall(accessToken, updatePayload, userIds);
successMessages.push(`Updated ${userIds.length} user(s)`);
successMessages.push(t("bulkEditUsers.notifications.updatedUsers", { count: userIds.length }));
}
}
@ -174,11 +176,13 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
if (successfulTeams.length > 0) {
const totalAdditions = successfulTeams.reduce((sum, r) => sum + r.successfulAdditions, 0);
successMessages.push(`Added users to ${successfulTeams.length} team(s) (${totalAdditions} total additions)`);
successMessages.push(
t("bulkEditUsers.notifications.addedToTeams", { count: successfulTeams.length, total: totalAdditions }),
);
}
if (failedTeams.length > 0) {
MessageManager.warning(`Failed to add users to ${failedTeams.length} team(s)`);
MessageManager.warning(t("bulkEditUsers.notifications.failedToAddToTeams", { count: failedTeams.length }));
}
}
@ -196,7 +200,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
onCancel();
} catch (error) {
console.error("Bulk operation failed:", error);
NotificationsManager.fromBackend("Failed to perform bulk operations");
NotificationsManager.fromBackend(t("bulkEditUsers.notifications.bulkOpFailed"));
} finally {
setLoading(false);
}
@ -207,18 +211,22 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
open={open}
onCancel={handleCancel}
footer={null}
title={updateAllUsers ? "Bulk Edit All Users" : `Bulk Edit ${selectedUsers.length} User(s)`}
title={
updateAllUsers
? t("bulkEditUsers.titleAllUsers")
: t("bulkEditUsers.titleSelectedUsers", { count: selectedUsers.length })
}
width={800}
>
{allowAllUsers && (
<div className="mb-4">
<Checkbox checked={updateAllUsers} onChange={(e) => setUpdateAllUsers(e.target.checked)}>
<Text strong>Update ALL users in the system</Text>
<Text strong>{t("bulkEditUsers.updateAllUsersLabel")}</Text>
</Checkbox>
{updateAllUsers && (
<div style={{ marginTop: 8 }}>
<Text type="warning" style={{ fontSize: "12px" }}>
⚠️ This will apply changes to ALL users in the system, not just the selected ones.
⚠️ {t("bulkEditUsers.updateAllUsersWarning")}
</Text>
</div>
)}
@ -227,7 +235,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
{!updateAllUsers && (
<div className="mb-4">
<Title level={5}>Selected Users ({selectedUsers.length}):</Title>
<Title level={5}>{t("bulkEditUsers.selectedUsersTitle", { count: selectedUsers.length })}</Title>
<Table
size="small"
bordered
@ -237,7 +245,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
rowKey="user_id"
columns={[
{
title: "User ID",
title: t("bulkEditUsers.columns.userId"),
dataIndex: "user_id",
key: "user_id",
width: "30%",
@ -248,18 +256,18 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
),
},
{
title: "Email",
title: t("bulkEditUsers.columns.email"),
dataIndex: "user_email",
key: "user_email",
width: "25%",
render: (text: string) => (
<Text type="secondary" style={{ fontSize: "12px" }}>
{text || "No email"}
{text || t("bulkEditUsers.noEmail")}
</Text>
),
},
{
title: "Current Role",
title: t("bulkEditUsers.columns.currentRole"),
dataIndex: "user_role",
key: "user_role",
width: "25%",
@ -268,12 +276,14 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
),
},
{
title: "Budget",
title: t("bulkEditUsers.columns.budget"),
dataIndex: "max_budget",
key: "max_budget",
width: "20%",
render: (budget: number | null) => (
<Text style={{ fontSize: "12px" }}>{budget !== null ? `$${budget}` : "Unlimited"}</Text>
<Text style={{ fontSize: "12px" }}>
{budget !== null ? `$${budget}` : t("bulkEditUsers.unlimited")}
</Text>
),
},
]}
@ -285,25 +295,29 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
<div className="mb-4">
<Text>
<strong>Instructions:</strong> Fill in the fields below with the values you want to apply to all selected
users. You can bulk edit: role, budget, models, and metadata. You can also add users to teams.
<strong>{t("bulkEditUsers.instructionsLabel")}</strong> {t("bulkEditUsers.instructions")}
</Text>
</div>
{/* Team Management Section */}
<Card title="Team Management" size="small" className="mb-4" style={{ backgroundColor: "#fafafa" }}>
<Card
title={t("bulkEditUsers.teamManagement.sectionTitle")}
size="small"
className="mb-4"
style={{ backgroundColor: "#fafafa" }}
>
<Space direction="vertical" style={{ width: "100%" }}>
<Checkbox checked={addToTeams} onChange={(e) => setAddToTeams(e.target.checked)}>
Add selected users to teams
{t("bulkEditUsers.teamManagement.addToTeamsLabel")}
</Checkbox>
{addToTeams && (
<>
<div>
<Text strong>Select Teams:</Text>
<Text strong>{t("bulkEditUsers.teamManagement.selectTeamsLabel")}</Text>
<Select
mode="multiple"
placeholder="Select teams to add users to"
placeholder={t("bulkEditUsers.teamManagement.selectTeamsPlaceholder")}
value={selectedTeams}
onChange={setSelectedTeams}
style={{ width: "100%", marginTop: 8 }}
@ -317,9 +331,9 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
</div>
<div>
<Text strong>Team Budget (Optional):</Text>
<Text strong>{t("bulkEditUsers.teamManagement.teamBudgetLabel")}</Text>
<InputNumber
placeholder="Max budget per user in team"
placeholder={t("bulkEditUsers.teamManagement.teamBudgetPlaceholder")}
value={teamBudget}
onChange={(value) => setTeamBudget(value)}
style={{ width: "100%", marginTop: 8 }}
@ -328,13 +342,12 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
precision={2}
/>
<Text type="secondary" style={{ fontSize: "12px" }}>
Leave empty for unlimited budget within team limits
{t("bulkEditUsers.teamManagement.teamBudgetHint")}
</Text>
</div>
<Text type="secondary" style={{ fontSize: "12px" }}>
Users will be added with &quot;user&quot; role by default. All users will be added to each selected
team.
{t("bulkEditUsers.teamManagement.defaultRoleNote")}
</Text>
</>
)}
@ -356,7 +369,11 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
{loading && (
<div style={{ textAlign: "center", marginTop: "10px" }}>
<Text>Updating {updateAllUsers ? "all users" : selectedUsers.length} user(s)...</Text>
<Text>
{updateAllUsers
? t("bulkEditUsers.updatingAllUsers")
: t("bulkEditUsers.updatingUsers", { count: selectedUsers.length })}
</Text>
</div>
)}
</Modal>

View file

@ -16,6 +16,7 @@ import {
Typography,
} from "antd";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import BulkCreateUsers from "./bulk_create_users_button";
import TeamDropdown from "./common_components/team_dropdown";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
@ -68,6 +69,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
onUserCreated,
isEmbedded = false,
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [uiSettings, setUISettings] = useState<UISettings | null>(null);
const [form] = Form.useForm();
@ -128,7 +130,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
send_invite_email?: boolean;
}) => {
try {
NotificationsManager.info("Making API Call");
NotificationsManager.info(t("createUserButton.notifications.makingApiCall"));
if (!isEmbedded) {
setIsModalVisible(true);
}
@ -175,11 +177,12 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
setIsInvitationLinkModalVisible(true);
}
NotificationsManager.success("API user Created");
NotificationsManager.success(t("createUserButton.notifications.userCreated"));
form.resetFields();
localStorage.removeItem("userData" + userID);
} catch (error: any) {
const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user";
const errorMessage =
error.response?.data?.detail || error?.message || t("createUserButton.notifications.errorCreatingUser");
NotificationsManager.fromBackend(errorMessage);
console.error("Error creating the user:", error);
}
@ -197,13 +200,12 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }}
>
<Alert
message="Email invitations"
message={t("createUserButton.emailInvitations")}
description={
<>
New users receive an email invite only when an email integration (SMTP, Resend, or SendGrid) is
configured.{" "}
{t("createUserButton.emailInvitationsDesc")}{" "}
<Link href="https://docs.litellm.ai/docs/proxy/email" target="_blank">
Learn how to set up email notifications
{t("createUserButton.learnHowToSetupEmail")}
</Link>
</>
}
@ -211,10 +213,10 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
showIcon
className="mb-4"
/>
<Form.Item label="User Email" name="user_email">
<Form.Item label={t("createUserButton.userEmailLabel")} name="user_email">
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="User Role" name="user_role">
<Form.Item label={t("createUserButton.userRoleLabel")} name="user_role">
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
@ -229,20 +231,20 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
))}
</Select2>
</Form.Item>
<Form.Item label="Team" name="team_id">
<Form.Item label={t("createUserButton.teamLabel")} name="team_id">
<TeamDropdown />
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
<Form.Item label={t("createUserButton.metadataLabel")} name="metadata">
<Input.TextArea rows={4} placeholder={t("createUserButton.metadataPlaceholder")} />
</Form.Item>
<Form.Item label="Send invitation email" name="send_invite_email" valuePropName="checked">
<Form.Item label={t("createUserButton.sendInviteEmailLabel")} name="send_invite_email" valuePropName="checked">
<Checkbox />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button htmlType="submit">Create User</Button>
<Button htmlType="submit">{t("createUserButton.createUserSubmitButton")}</Button>
</div>
</Form>
);
@ -252,11 +254,11 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
return (
<div className="flex gap-2">
<Button type="primary" className="mb-0" onClick={() => setIsModalVisible(true)}>
+ Invite User
{t("createUserButton.inviteUserButton")}
</Button>
<BulkCreateUsers accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
<Modal
title="Invite User"
title={t("createUserButton.inviteUserModalTitle")}
open={isModalVisible}
width={800}
footer={null}
@ -264,15 +266,14 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
onCancel={handleCancel}
>
<Space direction="vertical" size="middle">
<Text className="mb-1">Create a User who can own keys</Text>
<Text className="mb-1">{t("createUserButton.createUserOwnsKeys")}</Text>
<Alert
message="Email invitations"
message={t("createUserButton.emailInvitations")}
description={
<>
New users receive an email invite only when an email integration (SMTP, Resend, or SendGrid) is
configured.{" "}
{t("createUserButton.emailInvitationsDesc")}{" "}
<Link href="https://docs.litellm.ai/docs/proxy/email" target="_blank">
Learn how to set up email notifications
{t("createUserButton.learnHowToSetupEmail")}
</Link>
</>
}
@ -289,14 +290,14 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
labelAlign="left"
initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }}
>
<Form.Item label="User Email" name="user_email">
<Form.Item label={t("createUserButton.userEmailLabel")} name="user_email">
<Input />
</Form.Item>
<Form.Item
label={
<span>
Global Proxy Role{" "}
<Tooltip title="This role is independent of any team/org specific roles. Configure Team / Organization Admins in the Settings">
{t("createUserButton.globalProxyRoleLabel")}{" "}
<Tooltip title={t("createUserButton.globalProxyRoleTooltip")}>
<InfoCircleOutlined />
</Tooltip>
</span>
@ -318,20 +319,24 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
</Form.Item>
<Form.Item
label="Team"
label={t("createUserButton.teamLabel")}
className="gap-2"
name="team_id"
help="If selected, user will be added as a 'user' role to the team."
help={t("createUserButton.teamHelp")}
>
<TeamDropdown />
</Form.Item>
<Form.Item
label="Organization"
label={t("createUserButton.organizationLabel")}
name="organization_ids"
help="The user will be added to the selected organization(s)."
help={t("createUserButton.organizationHelp")}
>
<Select mode="multiple" placeholder="Select Organization" style={{ width: "100%" }}>
<Select
mode="multiple"
placeholder={t("createUserButton.selectOrganizationPlaceholder")}
style={{ width: "100%" }}
>
{organizations.map((org) => (
<Option key={org.organization_id} value={org.organization_id}>
{org.organization_alias} ({org.organization_id})
@ -340,36 +345,44 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
</Select>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
<Form.Item label={t("createUserButton.metadataLabel")} name="metadata">
<Input.TextArea rows={4} placeholder={t("createUserButton.metadataPlaceholder")} />
</Form.Item>
<Form.Item label="Send invitation email" name="send_invite_email" valuePropName="checked">
<Form.Item
label={t("createUserButton.sendInviteEmailLabel")}
name="send_invite_email"
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Accordion>
<AccordionHeader>
<Text strong>Personal Key Creation</Text>
<Text strong>{t("createUserButton.personalKeyCreation")}</Text>
</AccordionHeader>
<AccordionBody>
<Form.Item
className="gap-2"
label={
<span>
Models{" "}
<Tooltip title="Models user has access to, outside of team scope.">
{t("createUserButton.modelsLabel")}{" "}
<Tooltip title={t("createUserButton.modelsTooltip")}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="models"
help="Models user has access to, outside of team scope."
help={t("createUserButton.modelsHelp")}
>
<Select2 mode="multiple" placeholder="Select models" style={{ width: "100%" }}>
<Select2
mode="multiple"
placeholder={t("createUserButton.modelsPlaceholder")}
style={{ width: "100%" }}
>
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
{t("createUserButton.allProxyModels")}
</Select2.Option>
<Select2.Option key="no-default-models" value="no-default-models">
No Default Models
{t("createUserButton.noDefaultModels")}
</Select2.Option>
{userModels.map((model) => (
<Select2.Option key={model} value={model}>
@ -383,7 +396,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="primary" icon={<UserAddOutlined />} htmlType="submit">
Invite User
{t("createUserButton.inviteUserSubmitButton")}
</Button>
</div>
</Form>

View file

@ -3,12 +3,14 @@
import React from "react";
import { Alert } from "antd";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { Trans, useTranslation } from "react-i18next";
interface DebugWarningBannerProps {
accessToken: string | null;
}
export const DebugWarningBanner: React.FC<DebugWarningBannerProps> = ({ accessToken }) => {
const { t } = useTranslation();
const { data: healthData } = useHealthReadinessDetails(accessToken);
// Only show banner if detailed debug mode is explicitly enabled
@ -18,14 +20,8 @@ export const DebugWarningBanner: React.FC<DebugWarningBannerProps> = ({ accessTo
return (
<Alert
message="Performance Warning: Detailed Debug Mode Active"
description={
<>
Detailed debug logging (<code>LITELLM_LOG=DEBUG</code>) is currently enabled. This mode logs extensive
diagnostic information and will significantly degrade performance. It should only be used for troubleshooting
and disabled in production environments.
</>
}
message={t("debugWarningBanner.message")}
description={<Trans i18nKey="debugWarningBanner.description" components={{ code: <code /> }} />}
type="warning"
showIcon
banner

View file

@ -7,6 +7,7 @@ import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_compone
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import NotificationManager from "./molecules/notifications_manager";
import { useTranslation } from "react-i18next";
interface DefaultUserSettingsProps {
accessToken: string | null;
@ -27,6 +28,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
userID,
userRole,
}) => {
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(true);
const [settings, setSettings] = useState<any>(null);
const [isEditing, setIsEditing] = useState<boolean>(false);
@ -62,7 +64,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
}
} catch (error) {
console.error("Error fetching SSO settings:", error);
NotificationManager.fromBackend("Failed to fetch SSO settings");
NotificationManager.fromBackend(t("defaultUserSettings.notifications.fetchFailed"));
} finally {
setLoading(false);
}
@ -90,7 +92,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
setIsEditing(false);
} catch (error) {
console.error("Error updating SSO settings:", error);
NotificationManager.fromBackend("Failed to update settings: " + error);
NotificationManager.fromBackend(t("defaultUserSettings.notifications.updateFailed", { error: String(error) }));
} finally {
setSaving(false);
}
@ -158,29 +160,29 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
{normalizedTeams.map((team, index) => (
<div key={index} className="border rounded-lg p-4 bg-gray-50">
<div className="flex items-center justify-between mb-3">
<Text className="font-medium">Team {index + 1}</Text>
<Text className="font-medium">{t("defaultUserSettings.teamEntry.teamLabel", { number: index + 1 })}</Text>
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => removeTeam(index)}>
Remove
{t("defaultUserSettings.teamEntry.removeButton")}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<Text className="text-sm font-medium mb-1">Team ID</Text>
<Text className="text-sm font-medium mb-1">{t("defaultUserSettings.teamEntry.teamIdLabel")}</Text>
<TextInput
value={team.team_id}
onChange={(e) => updateTeam(index, "team_id", e.target.value)}
placeholder="Enter team ID"
placeholder={t("defaultUserSettings.teamEntry.teamIdPlaceholder")}
/>
</div>
<div>
<Text className="text-sm font-medium mb-1">Max Budget in Team</Text>
<Text className="text-sm font-medium mb-1">{t("defaultUserSettings.teamEntry.maxBudgetLabel")}</Text>
<InputNumber
style={{ width: "100%" }}
value={team.max_budget_in_team}
onChange={(value) => updateTeam(index, "max_budget_in_team", value)}
placeholder="Optional"
placeholder={t("defaultUserSettings.teamEntry.maxBudgetPlaceholder")}
min={0}
step={0.01}
precision={2}
@ -188,14 +190,14 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
</div>
<div>
<Text className="text-sm font-medium mb-1">User Role</Text>
<Text className="text-sm font-medium mb-1">{t("defaultUserSettings.teamEntry.userRoleLabel")}</Text>
<Select
style={{ width: "100%" }}
value={team.user_role}
onChange={(value) => updateTeam(index, "user_role", value)}
>
<Option value="user">User</Option>
<Option value="admin">Admin</Option>
<Option value="user">{t("defaultUserSettings.displayValues.userRoleOption")}</Option>
<Option value="admin">{t("defaultUserSettings.displayValues.adminRoleOption")}</Option>
</Select>
</div>
</div>
@ -203,7 +205,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
))}
<Button icon={<PlusOutlined />} onClick={addTeam} className="w-full">
Add Team
{t("defaultUserSettings.teamEntry.addTeamButton")}
</Button>
</div>
);
@ -314,10 +316,12 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
};
const renderValue = (key: string, value: any): JSX.Element => {
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
if (value === null || value === undefined)
return <span className="text-gray-400">{t("defaultUserSettings.displayValues.notSet")}</span>;
if (key === "teams" && Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-400">No teams assigned</span>;
if (value.length === 0)
return <span className="text-gray-400">{t("defaultUserSettings.displayValues.noTeamsAssigned")}</span>;
const normalizedTeams = normalizeTeams(value);
@ -327,19 +331,21 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
<div key={index} className="border rounded-lg p-3 bg-white">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
<div>
<span className="font-medium text-gray-600">Team ID:</span>
<p className="text-gray-900">{team.team_id || "Not specified"}</p>
<span className="font-medium text-gray-600">{t("defaultUserSettings.teamEntry.teamIdLabel")}:</span>
<p className="text-gray-900">{team.team_id || t("defaultUserSettings.displayValues.notSpecified")}</p>
</div>
<div>
<span className="font-medium text-gray-600">Max Budget:</span>
<span className="font-medium text-gray-600">
{t("defaultUserSettings.teamEntry.maxBudgetLabel")}:
</span>
<p className="text-gray-900">
{team.max_budget_in_team !== undefined
? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}`
: "No limit"}
: t("defaultUserSettings.displayValues.noLimit")}
</p>
</div>
<div>
<span className="font-medium text-gray-600">Role:</span>
<span className="font-medium text-gray-600">{t("defaultUserSettings.teamEntry.userRoleLabel")}:</span>
<p className="text-gray-900 capitalize">{team.user_role}</p>
</div>
</div>
@ -364,11 +370,11 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
}
if (typeof value === "boolean") {
return <span>{value ? "Enabled" : "Disabled"}</span>;
return <span>{value ? t("common.enabled") : t("common.disabled")}</span>;
}
if (key === "models" && Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-400">None</span>;
if (value.length === 0) return <span className="text-gray-400">{t("common.none")}</span>;
return (
<div className="flex flex-wrap gap-2 mt-1">
@ -383,7 +389,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
if (typeof value === "object") {
if (Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-400">None</span>;
if (value.length === 0) return <span className="text-gray-400">{t("common.none")}</span>;
return (
<div className="flex flex-wrap gap-2 mt-1">
@ -413,7 +419,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
if (!settings) {
return (
<Card>
<Text>No settings available or you do not have permission to view them.</Text>
<Text>{t("defaultUserSettings.noSettings")}</Text>
</Card>
);
}
@ -423,7 +429,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
const { values, field_schema } = settings;
if (!field_schema || !field_schema.properties) {
return <Text>No schema information available</Text>;
return <Text>{t("defaultUserSettings.noSchemaInfo")}</Text>;
}
return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => {
@ -434,7 +440,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
<Text className="font-medium text-lg">{displayName}</Text>
<Paragraph className="text-sm text-gray-500 mt-1">
{property.description || "No description available"}
{property.description || t("defaultUserSettings.noDescriptionAvailable")}
</Paragraph>
{isEditing ? (
@ -450,7 +456,7 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
return (
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Default User Settings</Title>
<Title>{t("defaultUserSettings.title")}</Title>
{!loading &&
settings &&
(isEditing ? (
@ -462,15 +468,15 @@ const DefaultUserSettings: React.FC<DefaultUserSettingsProps> = ({
}}
disabled={saving}
>
Cancel
{t("common.cancel")}
</Button>
<Button type="primary" onClick={handleSaveSettings} loading={saving}>
Save Changes
{t("defaultUserSettings.saveChanges")}
</Button>
</div>
) : (
<Button type="primary" onClick={() => setIsEditing(true)}>
Edit Settings
{t("defaultUserSettings.editSettings")}
</Button>
))}
</div>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Tag } from "antd";
import { GlobalOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
interface GuardrailSettingsViewProps {
globalGuardrailNames: Set<string>;
@ -19,6 +20,7 @@ export function GuardrailSettingsView({
variant = "card",
className = "",
}: GuardrailSettingsViewProps) {
const { t } = useTranslation();
const optedOutSet = new Set(optedOutGlobalGuardrails);
const globalsRunning = Array.from(globalGuardrailNames).filter((n) => !optedOutSet.has(n));
const nonGlobalOptIns = teamGuardrails.filter((n) => !globalGuardrailNames.has(n));
@ -26,16 +28,19 @@ export function GuardrailSettingsView({
const isEmpty = !killSwitchOn && globalsRunning.length === 0 && nonGlobalOptIns.length === 0;
const content = isEmpty ? (
<span className="block text-gray-500">No guardrails configured</span>
<span className="block text-gray-500">{t("guardrailSettingsView.noGuardrailsConfigured")}</span>
) : (
<div className="flex flex-col gap-4">
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">
<GlobalOutlined style={{ marginInlineEnd: 4 }} aria-label="Global guardrail" />
Global
<GlobalOutlined
style={{ marginInlineEnd: 4 }}
aria-label={t("guardrailSettingsView.globalGuardrailAriaLabel")}
/>
{t("guardrailSettingsView.globalLabel")}
</span>
{killSwitchOn ? (
<Tag color="gold">Bypassed for this team</Tag>
<Tag color="gold">{t("guardrailSettingsView.bypassedForTeam")}</Tag>
) : globalsRunning.length > 0 ? (
<div className="flex flex-wrap gap-2">
{globalsRunning.map((name) => (
@ -45,11 +50,13 @@ export function GuardrailSettingsView({
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
<span className="block text-sm text-gray-500">{t("guardrailSettingsView.noneConfigured")}</span>
)}
</div>
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">Team-specific</span>
<span className="block text-sm font-medium text-gray-700 mb-2">
{t("guardrailSettingsView.teamSpecificLabel")}
</span>
{nonGlobalOptIns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{nonGlobalOptIns.map((name) => (
@ -59,7 +66,7 @@ export function GuardrailSettingsView({
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
<span className="block text-sm text-gray-500">{t("guardrailSettingsView.noneConfigured")}</span>
)}
</div>
</div>
@ -70,10 +77,8 @@ export function GuardrailSettingsView({
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<span className="block font-semibold text-gray-900">Guardrails Settings</span>
<span className="block text-xs text-gray-500">
Global and team-specific guardrails applied to this team
</span>
<span className="block font-semibold text-gray-900">{t("guardrailSettingsView.cardTitle")}</span>
<span className="block text-xs text-gray-500">{t("guardrailSettingsView.cardSubtitle")}</span>
</div>
</div>
{content}
@ -83,7 +88,7 @@ export function GuardrailSettingsView({
return (
<div className={`${className}`}>
<span className="block font-medium text-gray-900 mb-3">Guardrails Settings</span>
<span className="block font-medium text-gray-900 mb-3">{t("guardrailSettingsView.inlineTitle")}</span>
{content}
</div>
);

View file

@ -393,6 +393,178 @@
"addMember": "Add Member"
}
},
"adminPanel": {
"title": "Admin Access",
"goToInternalUsers": "Go to 'Internal Users' page to add other admins.",
"allIPAddressesAllowed": "All IP Addresses Allowed",
"tabs": {
"ssoSettings": "SSO Settings",
"securitySettings": "Security Settings",
"scim": "SCIM",
"uiSettings": "UI Settings",
"loggingSettings": "Logging Settings",
"hashicorpVault": "Hashicorp Vault"
},
"securityCard": {
"title": "Security Settings",
"deprecatedAlertMessage": "SSO Configuration Deprecated",
"deprecatedAlertDescription": "Editing SSO Settings on this page is deprecated and will be removed in a future version. Please use the SSO Settings tab for SSO configuration.",
"editSSOSettings": "Edit SSO Settings",
"addSSO": "Add SSO",
"allowedIPs": "Allowed IPs",
"uiAccessControl": "UI Access Control"
},
"loginWithoutSSO": {
"title": "Login without SSO",
"description": "If you need to login without sso, you can access"
},
"manageAllowedIPModal": {
"title": "Manage Allowed IP Addresses",
"addIPAddress": "Add IP Address",
"ipAddressColumn": "IP Address",
"actionColumn": "Action"
},
"addIPModal": {
"title": "Add Allowed IP Address",
"inputPlaceholder": "Enter IP address",
"inputRequired": "Please enter an IP address",
"submitButton": "Add IP Address"
},
"deleteIPModal": {
"title": "Confirm Delete",
"confirmText": "Are you sure you want to delete the IP address: {{ip}}?"
},
"uiAccessControlModal": {
"title": "UI Access Control Settings",
"updateSuccess": "UI Access Control settings updated successfully"
},
"notifications": {
"premiumOnlyFeature": "This feature is only available for premium users. Please upgrade your account.",
"premiumOnlyUIAccessControl": "Only premium users can configure UI access control",
"ipAddedSuccess": "IP address added successfully",
"ipDeletedSuccess": "IP address deleted successfully"
},
"fetchAllowedIpsFailed": "Failed to fetch allowed IPs {{error}}",
"addIpFailed": "Failed to add IP address {{error}}",
"deleteIpFailed": "Failed to delete IP address {{error}}"
},
"bulkEditUsers": {
"titleAllUsers": "Bulk Edit All Users",
"titleSelectedUsers": "Bulk Edit {{count}} User(s)",
"updateAllUsersLabel": "Update ALL users in the system",
"updateAllUsersWarning": "This will apply changes to ALL users in the system, not just the selected ones.",
"selectedUsersTitle": "Selected Users ({{count}}):",
"columns": {
"userId": "User ID",
"email": "Email",
"currentRole": "Current Role",
"budget": "Budget"
},
"noEmail": "No email",
"unlimited": "Unlimited",
"instructions": "Fill in the fields below with the values you want to apply to all selected users. You can bulk edit: role, budget, models, and metadata. You can also add users to teams.",
"instructionsLabel": "Instructions:",
"teamManagement": {
"sectionTitle": "Team Management",
"addToTeamsLabel": "Add selected users to teams",
"selectTeamsLabel": "Select Teams:",
"selectTeamsPlaceholder": "Select teams to add users to",
"teamBudgetLabel": "Team Budget (Optional):",
"teamBudgetPlaceholder": "Max budget per user in team",
"teamBudgetHint": "Leave empty for unlimited budget within team limits",
"defaultRoleNote": "Users will be added with \"user\" role by default. All users will be added to each selected team."
},
"updatingUsers": "Updating {{count}} user(s)...",
"updatingAllUsers": "Updating all users...",
"notifications": {
"noFieldsModified": "Please modify at least one field or select teams to add users to",
"accessTokenNotFound": "Access token not found",
"bulkOpFailed": "Failed to perform bulk operations",
"updatedAllUsers": "Updated all users ({{total}} total)",
"updatedUsers": "Updated {{count}} user(s)",
"addedToTeams": "Added users to {{count}} team(s) ({{total}} total additions)",
"failedToAddToTeams": "Failed to add users to {{count}} team(s)"
}
},
"createUserButton": {
"inviteUserButton": "+ Invite User",
"createUserOwnsKeys": "Create a User who can own keys",
"emailInvitations": "Email invitations",
"emailInvitationsDesc": "New users receive an email invite only when an email integration (SMTP, Resend, or SendGrid) is configured.",
"learnHowToSetupEmail": "Learn how to set up email notifications",
"userEmailLabel": "User Email",
"userRoleLabel": "User Role",
"globalProxyRoleLabel": "Global Proxy Role",
"globalProxyRoleTooltip": "This role is independent of any team/org specific roles. Configure Team / Organization Admins in the Settings",
"teamLabel": "Team",
"teamHelp": "If selected, user will be added as a 'user' role to the team.",
"organizationLabel": "Organization",
"organizationHelp": "The user will be added to the selected organization(s).",
"selectOrganizationPlaceholder": "Select Organization",
"metadataLabel": "Metadata",
"metadataPlaceholder": "Enter metadata as JSON",
"sendInviteEmailLabel": "Send invitation email",
"personalKeyCreation": "Personal Key Creation",
"modelsLabel": "Models",
"modelsTooltip": "Models user has access to, outside of team scope.",
"modelsHelp": "Models user has access to, outside of team scope.",
"modelsPlaceholder": "Select models",
"allProxyModels": "All Proxy Models",
"noDefaultModels": "No Default Models",
"inviteUserModalTitle": "Invite User",
"inviteUserSubmitButton": "Invite User",
"createUserSubmitButton": "Create User",
"notifications": {
"makingApiCall": "Making API Call",
"userCreated": "API user Created",
"errorCreatingUser": "Error creating the user"
}
},
"debugWarningBanner": {
"message": "Performance Warning: Detailed Debug Mode Active",
"description": "Detailed debug logging (<code>LITELLM_LOG=DEBUG</code>) is currently enabled. This mode logs extensive diagnostic information and will significantly degrade performance. It should only be used for troubleshooting and disabled in production environments."
},
"defaultUserSettings": {
"title": "Default User Settings",
"editSettings": "Edit Settings",
"saveChanges": "Save Changes",
"noSettings": "No settings available or you do not have permission to view them.",
"noSchemaInfo": "No schema information available",
"noDescriptionAvailable": "No description available",
"teamEntry": {
"teamLabel": "Team {{number}}",
"removeButton": "Remove",
"teamIdLabel": "Team ID",
"teamIdPlaceholder": "Enter team ID",
"maxBudgetLabel": "Max Budget in Team",
"maxBudgetPlaceholder": "Optional",
"userRoleLabel": "User Role",
"addTeamButton": "Add Team"
},
"displayValues": {
"notSet": "Not set",
"noTeamsAssigned": "No teams assigned",
"notSpecified": "Not specified",
"noLimit": "No limit",
"userRoleOption": "User",
"adminRoleOption": "Admin"
},
"notifications": {
"fetchFailed": "Failed to fetch SSO settings",
"updateFailed": "Failed to update settings: {{error}}"
}
},
"guardrailSettingsView": {
"noGuardrailsConfigured": "No guardrails configured",
"globalLabel": "Global",
"globalGuardrailAriaLabel": "Global guardrail",
"bypassedForTeam": "Bypassed for this team",
"noneConfigured": "None configured",
"teamSpecificLabel": "Team-specific",
"cardTitle": "Guardrails Settings",
"cardSubtitle": "Global and team-specific guardrails applied to this team",
"inlineTitle": "Guardrails Settings"
},
"common": {
"save": "Save",
"cancel": "Cancel",

View file

@ -393,6 +393,178 @@
"addMember": "添加成员"
}
},
"adminPanel": {
"title": "管理员访问",
"goToInternalUsers": "前往「内部用户」页面添加其他管理员。",
"allIPAddressesAllowed": "允许所有 IP 地址",
"tabs": {
"ssoSettings": "SSO 设置",
"securitySettings": "安全设置",
"scim": "SCIM",
"uiSettings": "界面设置",
"loggingSettings": "日志设置",
"hashicorpVault": "Hashicorp Vault"
},
"securityCard": {
"title": "安全设置",
"deprecatedAlertMessage": "SSO 配置已弃用",
"deprecatedAlertDescription": "在此页面编辑 SSO 设置已弃用,将在未来版本中移除。请使用 SSO 设置标签页进行 SSO 配置。",
"editSSOSettings": "编辑 SSO 设置",
"addSSO": "添加 SSO",
"allowedIPs": "允许的 IP",
"uiAccessControl": "界面访问控制"
},
"loginWithoutSSO": {
"title": "不使用 SSO 登录",
"description": "如果您需要在不使用 SSO 的情况下登录,可以访问"
},
"manageAllowedIPModal": {
"title": "管理允许的 IP 地址",
"addIPAddress": "添加 IP 地址",
"ipAddressColumn": "IP 地址",
"actionColumn": "操作"
},
"addIPModal": {
"title": "添加允许的 IP 地址",
"inputPlaceholder": "输入 IP 地址",
"inputRequired": "请输入 IP 地址",
"submitButton": "添加 IP 地址"
},
"deleteIPModal": {
"title": "确认删除",
"confirmText": "确定要删除 IP 地址:{{ip}}?"
},
"uiAccessControlModal": {
"title": "界面访问控制设置",
"updateSuccess": "界面访问控制设置已成功更新"
},
"notifications": {
"premiumOnlyFeature": "此功能仅对高级用户开放,请升级您的账户。",
"premiumOnlyUIAccessControl": "只有高级用户才能配置界面访问控制",
"ipAddedSuccess": "IP 地址添加成功",
"ipDeletedSuccess": "IP 地址删除成功"
},
"fetchAllowedIpsFailed": "获取允许的 IP 列表失败 {{error}}",
"addIpFailed": "添加 IP 地址失败 {{error}}",
"deleteIpFailed": "删除 IP 地址失败 {{error}}"
},
"bulkEditUsers": {
"titleAllUsers": "批量编辑所有用户",
"titleSelectedUsers": "批量编辑 {{count}} 个用户",
"updateAllUsersLabel": "更新系统中的所有用户",
"updateAllUsersWarning": "这将对系统中的所有用户应用更改,而不仅仅是所选用户。",
"selectedUsersTitle": "已选用户({{count}} 人):",
"columns": {
"userId": "用户 ID",
"email": "邮箱",
"currentRole": "当前角色",
"budget": "预算"
},
"noEmail": "无邮箱",
"unlimited": "无限制",
"instructions": "在下方填写要应用于所有选定用户的值。可批量编辑:角色、预算、模型和元数据,也可将用户添加到团队。",
"instructionsLabel": "使用说明:",
"teamManagement": {
"sectionTitle": "团队管理",
"addToTeamsLabel": "将选定用户添加到团队",
"selectTeamsLabel": "选择团队:",
"selectTeamsPlaceholder": "选择要添加用户的团队",
"teamBudgetLabel": "团队预算(可选):",
"teamBudgetPlaceholder": "团队内每位用户的最大预算",
"teamBudgetHint": "留空表示在团队限制内不设预算上限",
"defaultRoleNote": "用户将以 user 角色默认添加,所有用户将被添加到每个选定的团队。"
},
"updatingUsers": "正在更新 {{count}} 个用户...",
"updatingAllUsers": "正在更新所有用户...",
"notifications": {
"noFieldsModified": "请至少修改一个字段或选择要添加用户的团队",
"accessTokenNotFound": "未找到访问令牌",
"bulkOpFailed": "批量操作失败",
"updatedAllUsers": "已更新所有用户(共 {{total}} 个)",
"updatedUsers": "已更新 {{count}} 个用户",
"addedToTeams": "已将用户添加到 {{count}} 个团队(共 {{total}} 次添加)",
"failedToAddToTeams": "向 {{count}} 个团队添加用户失败"
}
},
"createUserButton": {
"inviteUserButton": "+ 邀请用户",
"createUserOwnsKeys": "创建可拥有密钥的用户",
"emailInvitations": "邮件邀请",
"emailInvitationsDesc": "仅当配置了邮件集成(SMTP、Resend 或 SendGrid)时,新用户才会收到邮件邀请。",
"learnHowToSetupEmail": "了解如何设置邮件通知",
"userEmailLabel": "用户邮箱",
"userRoleLabel": "用户角色",
"globalProxyRoleLabel": "全局代理角色",
"globalProxyRoleTooltip": "此角色独立于任何团队/组织特定角色。在设置中配置团队/组织管理员。",
"teamLabel": "团队",
"teamHelp": "若选择,用户将以 user 角色添加到该团队。",
"organizationLabel": "组织",
"organizationHelp": "用户将被添加到所选组织。",
"selectOrganizationPlaceholder": "选择组织",
"metadataLabel": "元数据",
"metadataPlaceholder": "以 JSON 格式输入元数据",
"sendInviteEmailLabel": "发送邀请邮件",
"personalKeyCreation": "个人密钥创建",
"modelsLabel": "模型",
"modelsTooltip": "用户在团队范围外可访问的模型。",
"modelsHelp": "用户在团队范围外可访问的模型。",
"modelsPlaceholder": "选择模型",
"allProxyModels": "所有代理模型",
"noDefaultModels": "无默认模型",
"inviteUserModalTitle": "邀请用户",
"inviteUserSubmitButton": "邀请用户",
"createUserSubmitButton": "创建用户",
"notifications": {
"makingApiCall": "正在调用 API",
"userCreated": "API 用户已创建",
"errorCreatingUser": "创建用户时出错"
}
},
"debugWarningBanner": {
"message": "性能警告:详细调试模式已启用",
"description": "详细调试日志(<code>LITELLM_LOG=DEBUG</code>)当前已启用。此模式会记录大量诊断信息并显著降低性能,应仅用于排查问题,生产环境请禁用。"
},
"defaultUserSettings": {
"title": "默认用户设置",
"editSettings": "编辑设置",
"saveChanges": "保存更改",
"noSettings": "暂无可用设置,或您没有查看权限。",
"noSchemaInfo": "暂无 schema 信息",
"noDescriptionAvailable": "暂无描述",
"teamEntry": {
"teamLabel": "团队 {{number}}",
"removeButton": "移除",
"teamIdLabel": "团队 ID",
"teamIdPlaceholder": "输入团队 ID",
"maxBudgetLabel": "团队内最大预算",
"maxBudgetPlaceholder": "可选",
"userRoleLabel": "用户角色",
"addTeamButton": "添加团队"
},
"displayValues": {
"notSet": "未设置",
"noTeamsAssigned": "未分配团队",
"notSpecified": "未指定",
"noLimit": "无上限",
"userRoleOption": "用户",
"adminRoleOption": "管理员"
},
"notifications": {
"fetchFailed": "获取 SSO 设置失败",
"updateFailed": "更新设置失败:{{error}}"
}
},
"guardrailSettingsView": {
"noGuardrailsConfigured": "未配置护栏",
"globalLabel": "全局",
"globalGuardrailAriaLabel": "全局护栏",
"bypassedForTeam": "已为此团队绕过",
"noneConfigured": "未配置",
"teamSpecificLabel": "团队专属",
"cardTitle": "护栏设置",
"cardSubtitle": "应用于此团队的全局和团队专属护栏",
"inlineTitle": "护栏设置"
},
"common": {
"save": "保存",
"cancel": "取消",