From 2feef1f636d4db8086c8a5d3797becdff775500d Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Sat, 31 Oct 2026 11:53:34 -0700 Subject: [PATCH] refactor(ui): move teams, users and orgs text onto i18n Moves the page-authored English on /teams, /users and /organizations, plus the shared member table, user search modal, model select and budget schedule helpers they render, into react-i18next t() calls backed by en.json. The rendered English is unchanged Helpers that files outside this batch or untouched tests still call keep their signatures and take an optional t that defaults to the shared instance, the same as usageUnits.ts. Text that existing tests pin, values compared against the API, code samples, CSV column names and date formats stay as they were --- .../organizations/OrganizationFilters.tsx | 6 +- .../_components/OrganizationsPanel.tsx | 39 +- .../_components/OrganizationsTable.tsx | 15 +- .../_components/OrganizationsTableColumns.tsx | 71 +- .../users/_components/BulkEditUsers.tsx | 79 +- .../DefaultUserSettingsForm.tsx | 176 +- .../default-user-settings/schema.ts | 17 +- .../users/_components/user_edit_view.tsx | 100 +- .../users/_components/view_users.tsx | 38 +- .../_components/view_users/UsersTable.tsx | 56 +- .../view_users/UsersTableColumns.tsx | 110 +- .../_components/view_users/user_info_view.tsx | 187 +- .../src/components/CreateUserButton.tsx | 98 +- .../components/ModelSelect/ModelSelect.tsx | 47 +- .../src/components/TeamSSOSettings.tsx | 82 +- ui/litellm-dashboard/src/components/Teams.tsx | 319 ++-- .../TeamsPage/TeamMemberSpendBudgetCell.tsx | 29 +- .../src/components/TeamsPage/TeamsTable.tsx | 48 +- .../components/TeamsPage/teamTableColumns.tsx | 91 +- .../components/bulk_create_users_button.tsx | 220 +-- .../Filters/FiltersButton.tsx | 11 +- .../Filters/ResetFiltersButton.tsx | 6 +- .../common_components/MemberTable.tsx | 50 +- .../common_components/user_search_modal.tsx | 56 +- .../org-create/OrgCreateDialog.tsx | 62 +- .../org-settings/OrgSettingsForm.tsx | 66 +- .../organization/org-settings/schema.ts | 44 +- .../organization/organization_view.tsx | 140 +- .../components/team/AvailableTeamsPanel.tsx | 6 +- .../components/team/AvailableTeamsTable.tsx | 31 +- .../team/AvailableTeamsTableColumns.tsx | 33 +- .../src/components/team/EditMembership.tsx | 49 +- .../src/components/team/GuardrailsSelect.tsx | 25 +- .../src/components/team/LoggingSettings.tsx | 90 +- .../src/components/team/MyUserTab.tsx | 52 +- .../team/ResetMemberBudgetsDialog.tsx | 21 +- .../components/team/TeamAdminSettingsForm.tsx | 30 +- .../src/components/team/TeamInfo.tsx | 824 ++++---- .../src/components/team/TeamMemberTab.tsx | 138 +- .../components/team/TeamVirtualKeysTable.tsx | 190 +- .../src/components/team/memberFormValues.ts | 3 +- .../components/team/member_permissions.tsx | 30 +- .../components/team/teamAdminEditAccess.ts | 42 +- .../src/components/team/teamModelAccess.ts | 27 +- .../components/team/useMemberBudgetReset.ts | 17 +- ui/litellm-dashboard/src/i18n/locales/en.json | 1651 +++++++++++++++++ ui/litellm-dashboard/src/utils/budgetUtils.ts | 24 +- 47 files changed, 3946 insertions(+), 1600 deletions(-) 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 }) => ( -