diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx index 6ad2f00fdb0..cd5c796b569 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx @@ -2,6 +2,7 @@ import { FilterInput } from "@/components/common_components/Filters/FilterInput" import { FiltersButton } from "@/components/common_components/Filters/FiltersButton"; import { ResetFiltersButton } from "@/components/common_components/Filters/ResetFiltersButton"; import { Search, User } from "lucide-react"; +import { useTranslation } from "react-i18next"; interface OrganizationFiltersProps { filters: FilterState; @@ -23,6 +24,7 @@ const OrganizationFilters = ({ onChange, onReset, }: OrganizationFiltersProps) => { + const { t } = useTranslation(); const hasActiveFilters = !!(filters.org_id || filters.org_alias); return ( @@ -30,7 +32,7 @@ const OrganizationFilters = ({ {/* Search and Filter Controls */}
onChange("org_alias", value)} icon={Search} @@ -50,7 +52,7 @@ const OrganizationFilters = ({ {showFilters && (
onChange("org_id", value)} icon={User} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx index 2810902bef6..819aa6c3334 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx @@ -4,6 +4,7 @@ import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organization import { useQueryClient } from "@tanstack/react-query"; import { parseAsString, parseAsStringLiteral, useQueryStates } from "nuqs"; import React, { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import { toast } from "@/lib/toast"; import { organizationDeleteCall } from "@/components/networking"; @@ -28,6 +29,7 @@ const ORGANIZATION_DETAIL_STATE = { const ORGANIZATION_DETAIL_URL_KEYS = { tab: ORGANIZATION_TAB_URL_KEY }; const OrganizationsPanel: React.FC = ({ userRole, accessToken, premiumUser }) => { + const { t } = useTranslation(); const [{ org: selectedOrgId }, setOrganizationDetail] = useQueryStates(ORGANIZATION_DETAIL_STATE, { history: "push", urlKeys: ORGANIZATION_DETAIL_URL_KEYS, @@ -78,7 +80,7 @@ const OrganizationsPanel: React.FC = ({ userRole, acces try { setIsDeleting(true); await organizationDeleteCall(accessToken, orgToDelete); - toast.success("Organization deleted successfully"); + toast.success(t("organizations.panel.toasts.deleted")); setIsDeleteModalOpen(false); setOrgToDelete(null); @@ -99,16 +101,19 @@ const OrganizationsPanel: React.FC = ({ userRole, acces return (

- This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "} - - here - - . + + ), + }} + />

); @@ -118,7 +123,7 @@ const OrganizationsPanel: React.FC = ({ userRole, acces
{(userRole === "Admin" || userRole === "Org Admin") && ( )} @@ -133,7 +138,7 @@ const OrganizationsPanel: React.FC = ({ userRole, acces /> ) : ( <> -

Click on an organization ID to view its details.

+

{t("organizations.panel.listHint")}

= ({ userRole, acces @@ -27,12 +29,12 @@ function EmptyState({ searchActive }: { searchActive: boolean }) {
- {searchActive ? "No matching organizations" : "No organizations yet"} + {searchActive ? t("organizations.table.empty.noMatchesTitle") : t("organizations.table.empty.noneTitle")}
{searchActive - ? "No organizations match your search. Try a different name or ID." - : "Create an organization to group teams, models, and budgets."} + ? t("organizations.table.empty.noMatchesDescription") + : t("organizations.table.empty.noneDescription")}
); @@ -47,12 +49,13 @@ const OrganizationsTable: React.FC = ({ onEditClick, onDeleteClick, }) => { + const { t } = useTranslation(); const { sorting, onSortingChange, pagination, onPaginationChange } = useOrganizationsTableState(); const columns = useMemo(() => { - const deps = { userRole, onOrganizationClick, onEditClick, onDeleteClick }; + const deps = { userRole, onOrganizationClick, onEditClick, onDeleteClick, t }; return getOrganizationsTableColumns(deps); - }, [userRole, onOrganizationClick, onEditClick, onDeleteClick]); + }, [userRole, onOrganizationClick, onEditClick, onDeleteClick, t]); return ( = ({ sorting={sorting} onSortingChange={onSortingChange} isLoading={isLoading} - loadingMessage="Loading organizations…" + loadingMessage={t("organizations.table.loading")} noDataMessage={} size="compact" /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx index dcbf141314b..b4464392ea8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx @@ -1,7 +1,9 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import type { TFunction } from "i18next"; import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { DataTableMultiSortHeader, DataTableSortHeader } from "@/components/shared/DataTable"; import { DateCell, IdentityCell, ModelsCell, MoneyCell } from "@/components/shared/table_cells"; @@ -25,11 +27,18 @@ const getOrganizationBudget = (organization: Organization): OrganizationBudget = (organization.litellm_budget_table ?? {}) as OrganizationBudget; function OrganizationLimitsCell({ organization }: { organization: Organization }) { + const { t } = useTranslation(); const { tpm_limit, rpm_limit } = getOrganizationBudget(organization); return (
- TPM: {tpm_limit ?? "Unlimited"} - RPM: {rpm_limit ?? "Unlimited"} + + {t("organizations.table.limits.tpmLabel")} + {tpm_limit ?? t("organizations.table.limits.unlimited")} + + + {t("organizations.table.limits.rpmLabel")} + {rpm_limit ?? t("organizations.table.limits.unlimited")} +
); } @@ -41,10 +50,11 @@ interface OrganizationRowActionsProps { } function OrganizationRowActions({ organization, onEditClick, onDeleteClick }: OrganizationRowActionsProps) { + const { t } = useTranslation(); return ( @@ -56,7 +66,7 @@ function OrganizationRowActions({ organization, onEditClick, onDeleteClick }: Or onClick={() => onEditClick(organization.organization_id)} > - Edit + {t("organizations.table.actions.edit")} onDeleteClick(organization.organization_id)} > - Delete + {t("organizations.table.actions.delete")} @@ -76,6 +86,7 @@ export interface OrganizationsTableColumnsDeps { onOrganizationClick: (organizationId: string) => void; onEditClick: (organizationId: string) => void; onDeleteClick: (organizationId: string) => void; + t: TFunction; } export const getOrganizationsTableColumns = ({ @@ -83,12 +94,13 @@ export const getOrganizationsTableColumns = ({ onOrganizationClick, onEditClick, onDeleteClick, + t, }: OrganizationsTableColumnsDeps): ColumnDef[] => [ { id: "organization_id", accessorKey: "organization_id", - meta: { title: "Organization ID" }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.id") }, + header: ({ column }) => , size: 220, enableSorting: true, cell: ({ row }) => ( @@ -103,8 +115,8 @@ export const getOrganizationsTableColumns = ({ { id: "organization_alias", accessorKey: "organization_alias", - meta: { title: "Organization Name" }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.name") }, + header: ({ column }) => , size: 200, enableSorting: true, cell: ({ row }) => { @@ -120,8 +132,8 @@ export const getOrganizationsTableColumns = ({ id: "created_at", accessorKey: "created_at", sortingFn: "datetime", - meta: { title: "Created" }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.created") }, + header: ({ column }) => , size: 130, enableSorting: true, cell: ({ row }) => , @@ -129,8 +141,8 @@ export const getOrganizationsTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.spend"), numeric: true }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => , @@ -140,12 +152,17 @@ export const getOrganizationsTableColumns = ({ accessorFn: (organization) => getOrganizationBudget(organization).max_budget ?? Number.POSITIVE_INFINITY, sortUndefined: "last", sortDescFirst: true, - meta: { title: "Budget (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.budget"), numeric: true }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => ( - + ), }, { @@ -156,21 +173,21 @@ export const getOrganizationsTableColumns = ({ : undefined, sortUndefined: "last", sortDescFirst: false, - meta: { title: "Models", skeleton: "chips" }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.models"), skeleton: "chips" }, + header: ({ column }) => , size: 260, enableSorting: true, cell: ({ row }) => , }, { id: "limits", - meta: { title: "TPM / RPM Limits" }, + meta: { title: t("organizations.table.columns.limits") }, header: ({ table }) => ( ), @@ -183,11 +200,15 @@ export const getOrganizationsTableColumns = ({ accessorFn: (organization) => organization.members?.length ?? 0, sortUndefined: "last", sortDescFirst: true, - meta: { title: "Members", numeric: true }, - header: ({ column }) => , + meta: { title: t("organizations.table.columns.members"), numeric: true }, + header: ({ column }) => , size: 100, enableSorting: true, - cell: ({ row }) => {row.original.members?.length ?? 0} Members, + cell: ({ row }) => ( + + {t("organizations.table.memberCount", { count: row.original.members?.length ?? 0 })} + + ), }, ...(["tpm_limit", "rpm_limit"] as const).map( (field): ColumnDef => ({ @@ -201,7 +222,7 @@ export const getOrganizationsTableColumns = ({ { id: "actions", meta: { className: "text-right", headerClassName: "text-right" }, - header: () => Actions, + header: () => {t("organizations.table.columns.actions")}, size: 64, enableSorting: false, enableHiding: false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx index 67a648dacd1..b38dd5c6d5e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx @@ -1,4 +1,5 @@ import React, { useId, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { userBulkUpdateUserCall, teamBulkMemberAddCall, Member } from "@/components/networking"; import { UserEditView } from "./user_edit_view"; import { toast } from "@/lib/toast"; @@ -46,6 +47,7 @@ const BulkEditUserModal: React.FC = ({ userModels, allowAllUsers = false, }) => { + const { t } = useTranslation(); const { premiumUser } = useAuthorized(); const [loading, setLoading] = useState(false); const [selectedTeams, setSelectedTeams] = useState([]); @@ -89,7 +91,7 @@ const BulkEditUserModal: React.FC = ({ const handleSubmit = async (formValues: any) => { if (!accessToken) { - toast.fromError("Access token not found"); + toast.fromError(t("users.bulkEdit.toasts.noAccessToken")); return; } @@ -125,7 +127,7 @@ const BulkEditUserModal: React.FC = ({ const hasTeamAdditions = addToTeams && selectedTeams.length > 0; if (!hasUserUpdates && !hasTeamAdditions) { - toast.fromError("Please modify at least one field or select teams to add users to"); + toast.fromError(t("users.bulkEdit.toasts.nothingToUpdate")); return; } @@ -135,10 +137,10 @@ const BulkEditUserModal: React.FC = ({ if (hasUserUpdates) { if (updateAllUsers) { const result = await userBulkUpdateUserCall(accessToken, updatePayload, undefined, true); - successMessages.push(`Updated all users (${result.total_requested} total)`); + successMessages.push(t("users.bulkEdit.toasts.updatedAll", { total: result.total_requested })); } else { await userBulkUpdateUserCall(accessToken, updatePayload, userIds); - successMessages.push(`Updated ${userIds.length} user(s)`); + successMessages.push(t("users.bulkEdit.toasts.updated", { count: userIds.length })); } } @@ -190,11 +192,13 @@ const BulkEditUserModal: React.FC = ({ 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("users.bulkEdit.toasts.addedToTeams", { count: successfulTeams.length, additions: totalAdditions }), + ); } if (failedTeams.length > 0) { - toast.warning(`Failed to add users to ${failedTeams.length} team(s)`); + toast.warning(t("users.bulkEdit.toasts.teamAddFailed", { count: failedTeams.length })); } } @@ -212,7 +216,7 @@ const BulkEditUserModal: React.FC = ({ onCancel(); } catch (error) { console.error("Bulk operation failed:", error); - toast.fromError("Failed to perform bulk operations"); + toast.fromError(t("users.bulkEdit.toasts.bulkFailed")); } finally { setLoading(false); } @@ -223,7 +227,7 @@ const BulkEditUserModal: React.FC = ({ - {updateAllUsers ? "Bulk Edit All Users" : `Bulk Edit ${selectedUsers.length} User(s)`} + {updateAllUsers ? t("users.bulkEdit.titleAll") : t("users.bulkEdit.title", { count: selectedUsers.length })} {allowAllUsers && ( @@ -233,17 +237,15 @@ const BulkEditUserModal: React.FC = ({ id={updateAllUsersId} checked={updateAllUsers} onCheckedChange={(checked) => setUpdateAllUsers(checked === true)} - aria-label="Update ALL users in the system" + aria-label={t("users.bulkEdit.updateAll")} />
{updateAllUsers && (
- - ⚠️ This will apply changes to ALL users in the system, not just the selected ones. - + {t("users.bulkEdit.updateAllWarning")}
)} @@ -251,15 +253,19 @@ const BulkEditUserModal: React.FC = ({ {!updateAllUsers && (
-
Selected Users ({selectedUsers.length}):
+
+ {t("users.bulkEdit.selectedUsers", { count: selectedUsers.length })} +
- User ID - Email - Current Role - Budget + {t("users.bulkEdit.table.userId")} + {t("users.bulkEdit.table.email")} + {t("users.bulkEdit.table.currentRole")} + + {t("users.bulkEdit.table.budget")} + @@ -268,12 +274,19 @@ const BulkEditUserModal: React.FC = ({ {user.user_id.length > 20 ? `${user.user_id.slice(0, 20)}...` : user.user_id} - {user.user_email || "No email"} + + {user.user_email || t("users.bulkEdit.table.noEmail")} + {possibleUIRoles?.[user.user_role]?.ui_label || user.user_role} - + ))} @@ -287,15 +300,14 @@ const BulkEditUserModal: React.FC = ({

- 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. + }} />

{/* Team Management Section */} - Team Management + {t("users.bulkEdit.teamManagement.title")}
@@ -304,10 +316,10 @@ const BulkEditUserModal: React.FC = ({ id={addToTeamsId} checked={addToTeams} onCheckedChange={(checked) => setAddToTeams(checked === true)} - aria-label="Add selected users to teams" + aria-label={t("users.bulkEdit.teamManagement.addToTeams")} />
@@ -315,12 +327,12 @@ const BulkEditUserModal: React.FC = ({ <>
= ({
) => setTeamBudget(event.target.value === "" ? null : Number(event.target.value)) @@ -348,13 +360,12 @@ const BulkEditUserModal: React.FC = ({ step={0.01} /> - Leave empty for unlimited budget within team limits + {t("users.bulkEdit.teamManagement.teamBudgetHint")}
- Users will be added with "user" role by default. All users will be added to each selected - team. + {t("users.bulkEdit.teamManagement.defaultRoleNote")} )} @@ -379,7 +390,9 @@ const BulkEditUserModal: React.FC = ({ {loading && (
- Updating {updateAllUsers ? "all users" : selectedUsers.length} user(s)... + {updateAllUsers + ? t("users.bulkEdit.updatingAll") + : t("users.bulkEdit.updating", { count: selectedUsers.length })}
)} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx index 0239844851f..a617d5f84cb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx @@ -1,8 +1,10 @@ "use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { ParseKeys } from "i18next"; import * as React from "react"; import { useFieldArray, type Control } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { ModelSelect, MODEL_SENTINEL_OPTIONS } from "@/components/ModelSelect/ModelSelect"; @@ -29,21 +31,21 @@ import { const NO_RESET = "never"; -const BUDGET_DURATION_OPTIONS = [ - { value: NO_RESET, label: "No reset" }, - { value: "1h", label: "hourly" }, - { value: "24h", label: "daily" }, - { value: "7d", label: "weekly" }, - { value: "30d", label: "monthly" }, -] as const; +const BUDGET_DURATION_OPTIONS: readonly { value: string; labelKey: ParseKeys }[] = [ + { value: NO_RESET, labelKey: "users.defaultSettings.budgetDurations.noReset" }, + { value: "1h", labelKey: "users.defaultSettings.budgetDurations.hourly" }, + { value: "24h", labelKey: "users.defaultSettings.budgetDurations.daily" }, + { value: "7d", labelKey: "users.defaultSettings.budgetDurations.weekly" }, + { value: "30d", labelKey: "users.defaultSettings.budgetDurations.monthly" }, +]; -const TEAM_ROLE_OPTIONS = [ - { value: "user", label: "User" }, - { value: "admin", label: "Admin" }, -] as const; +const TEAM_ROLE_OPTIONS: readonly { value: "user" | "admin"; labelKey: ParseKeys }[] = [ + { value: "user", labelKey: "users.defaultSettings.teamRoles.user" }, + { value: "admin", labelKey: "users.defaultSettings.teamRoles.admin" }, +]; -const MODEL_SENTINEL_LABELS: ReadonlyMap = new Map( - MODEL_SENTINEL_OPTIONS.map(({ value, label }) => [value, label]), +const MODEL_SENTINEL_LABEL_KEYS: ReadonlyMap = new Map( + MODEL_SENTINEL_OPTIONS.map(({ value, labelKey }) => [value, labelKey]), ); const TEAMS_PAGE_SIZE = 50; @@ -71,6 +73,7 @@ interface RoleOption { type SettingsControl = Control; const TeamPickerField = ({ control, index }: { control: SettingsControl; index: number }) => { + const { t } = useTranslation(); const [search, setSearch] = React.useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteTeams( TEAMS_PAGE_SIZE, @@ -90,7 +93,7 @@ const TeamPickerField = ({ control, index }: { control: SettingsControl; index: ); return ( - + {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => ( { + const { t } = useTranslation(); const { fields, append, remove } = useFieldArray({ control, name: "teams" }); + const teamRoleItems = TEAM_ROLE_OPTIONS.map((option) => ({ value: option.value, label: t(option.labelKey) })); return (
-

Default Teams

-

- New users are added to these teams. Only teams that already exist can be selected. -

+

{t("users.defaultSettings.fields.teams")}

+

{t("users.defaultSettings.teams.description")}

{fields.map((field, index) => (
-

Team {index + 1}

+

{t("users.defaultSettings.teams.rowTitle", { number: index + 1 })}

- + {({ ref, ...budgetField }) => ( - + )} - + {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => ( - onChange(selected ?? "user")}> { - {TEAM_ROLE_OPTIONS.map((option) => ( + {teamRoleItems.map((option) => ( {option.label} @@ -172,7 +186,7 @@ const TeamsField = ({ control }: { control: SettingsControl }) => { ))}
); @@ -191,30 +205,41 @@ interface SettingsViewProps { } const SettingsView = ({ values, roleOptions }: SettingsViewProps) => { + const { t } = useTranslation(); const roleLabel = roleOptions.find((option) => option.value === values.user_role)?.label ?? values.user_role; const durationValue = values.budget_duration === "" ? NO_RESET : values.budget_duration; - const durationLabel = - BUDGET_DURATION_OPTIONS.find((option) => option.value === durationValue)?.label ?? values.budget_duration; + const durationOption = BUDGET_DURATION_OPTIONS.find((option) => option.value === durationValue); + const durationLabel = durationOption === undefined ? values.budget_duration : t(durationOption.labelKey); + const notSet = t("users.defaultSettings.view.notSet"); return (
- {roleLabel === "" ? "Not set" : roleLabel} - {values.max_budget === "" ? "Not set" : values.max_budget} - {durationLabel} - + {roleLabel === "" ? notSet : roleLabel} + + {values.max_budget === "" ? notSet : values.max_budget} + + {durationLabel} + {values.models.length === 0 - ? "Not set" - : values.models.map((model) => MODEL_SENTINEL_LABELS.get(model) ?? model).join(", ")} + ? notSet + : values.models + .map((model) => { + const labelKey = MODEL_SENTINEL_LABEL_KEYS.get(model); + return labelKey === undefined ? model : t(labelKey); + }) + .join(", ")}
-

Default Teams

+

{t("users.defaultSettings.fields.teams")}

{values.teams.length === 0 ? ( -

None

+

{t("users.defaultSettings.view.none")}

) : ( values.teams.map((team) => (

{team.team_id} - {team.max_budget_in_team !== "" && <> · ${team.max_budget_in_team} max budget} + {team.max_budget_in_team !== "" && ( + <> · {t("users.defaultSettings.view.teamMaxBudget", { budget: team.max_budget_in_team })} + )} <> · {team.user_role}

)) @@ -233,20 +258,25 @@ interface SettingsFormProps { } const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, onSaved }: SettingsFormProps) => { + const { t } = useTranslation(); const queryClient = useQueryClient(); + const budgetDurationItems = BUDGET_DURATION_OPTIONS.map((option) => ({ + value: option.value, + label: t(option.labelKey), + })); const form = useZodForm(defaultUserSettingsSchema, { defaultValues: initialValues }); const { isDirty } = form.formState; const mutation = useMutation({ mutationFn: (values: DefaultUserSettingsSubmitValues) => updateSettings(buildBody(values)), onSuccess: (_result, values) => { - toast.success("Default user settings updated successfully"); + toast.success(t("users.defaultSettings.toasts.updated")); queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY }); form.reset(values); onSaved(); }, onError: (error: unknown) => - toast.fromError(error instanceof Error ? error.message : "Failed to update default user settings"), + toast.fromError(error instanceof Error ? error.message : t("users.defaultSettings.toasts.updateFailed")), }); const onSubmit = form.handleSubmit((values) => mutation.mutate(values)); @@ -257,8 +287,8 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => ( } @@ -295,12 +325,12 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => ( } )} {!isBulkEdit && ( - + {({ ref, value, ...control }) => } )} - + {({ ref, value, ...control }) => } @@ -242,8 +247,8 @@ export function UserEditView({ control={form.control} name="user_role" label={labelWithHint( - "Global Proxy Role", - "This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.", + t("users.editView.fields.globalProxyRole"), + t("users.editView.fields.globalProxyRoleHint"), )} > {({ id, value, onChange }) => ( @@ -271,8 +276,8 @@ export function UserEditView({ control={form.control} name="models" label={labelWithHint( - "Personal Models", - "Select which models this user can access outside of team-scope. Choose 'All Proxy Models' to grant access to all models available on the proxy.", + t("users.editView.fields.personalModels"), + t("users.editView.fields.personalModelsHint"), )} > {({ value, onChange }) => ( @@ -280,7 +285,7 @@ export function UserEditView({ options={modelOptions} value={value} onValueChange={onChange} - placeholder="Select models" + placeholder={t("users.editView.fields.modelsPlaceholder")} disabled={!all_admin_roles.includes(userRole || "")} /> )} @@ -291,10 +296,10 @@ export function UserEditView({ name="max_budget" label={ <> - Max Budget (USD) + {t("users.editView.fields.maxBudget")} } @@ -308,19 +313,19 @@ export function UserEditView({ value={value ?? ""} onChange={(event) => onChange(event.target.value)} onWheel={(event) => event.currentTarget.blur()} - placeholder="Enter a numerical value" + placeholder={t("users.editView.fields.maxBudgetPlaceholder")} disabled={unlimitedBudget} /> )} - + {({ id, value, onChange }) => ( )} @@ -330,10 +335,7 @@ export function UserEditView({ {({ ref, value, onChange, ...control }) => ( onChange(event.target.value)} onWheel={(event) => event.currentTarget.blur()} - placeholder="Unlimited" + placeholder={t("users.editView.fields.rateLimitPlaceholder")} /> )} @@ -353,10 +355,7 @@ export function UserEditView({ {({ ref, value, onChange, ...control }) => ( onChange(event.target.value)} onWheel={(event) => event.currentTarget.blur()} - placeholder="Unlimited" + placeholder={t("users.editView.fields.rateLimitPlaceholder")} /> )} @@ -385,13 +384,19 @@ export function UserEditView({ onChange={setModelMaxBudget} availableModels={userModels} usage={userData.user_info?.model_max_budget_usage} - hint="Cap this user's spend on individual models, each with its own reset window. Applies across every key the user holds." + hint={t("users.editView.fields.modelMaxBudgetHint")} /> )} - + {({ ref, value, ...control }) => ( -