feat(ui): i18n for user dashboard, user edit, and user spend views

This commit is contained in:
吹雪 2026-06-10 14:28:55 +09:00
parent 33ac749174
commit e93492321a
6 changed files with 161 additions and 40 deletions

View file

@ -4,6 +4,7 @@ import { Col, Grid } from "@tremor/react";
import { jwtDecode } from "jwt-decode";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Onboarding from "../app/onboarding/page";
import { fetchTeams } from "./common_components/fetch_teams";
import { KeyResponse, Team } from "./key_team_helpers/key_list";
@ -75,6 +76,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
autoOpenCreate,
prefillData,
}) => {
const { t } = useTranslation();
const [userSpendData, setUserSpendData] = useState<UserInfo | null>(null);
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
@ -309,7 +311,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
}
if (userID == null) {
return <h1>User ID is not set</h1>;
return <h1>{t("userDashboard.userIdNotSet")}</h1>;
}
if (userRole == null) {

View file

@ -2,6 +2,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, SelectItem, TextInput, Textarea } from "@tremor/react";
import { Checkbox, Form, Select, Tooltip } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { all_admin_roles } from "../utils/roles";
import BudgetDurationDropdown from "./common_components/budget_duration_dropdown";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
@ -32,6 +33,7 @@ export function UserEditView({
possibleUIRoles,
isBulkEdit = false,
}: UserEditViewProps) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
@ -82,26 +84,26 @@ export function UserEditView({
return (
<Form form={form} onFinish={handleSubmit} layout="vertical">
{!isBulkEdit && (
<Form.Item label="User ID" name="user_id">
<Form.Item label={t("userEditView.userIdLabel")} name="user_id">
<TextInput disabled />
</Form.Item>
)}
{!isBulkEdit && (
<Form.Item label="Email" name="user_email">
<Form.Item label={t("userEditView.emailLabel")} name="user_email">
<TextInput />
</Form.Item>
)}
<Form.Item label="User Alias" name="user_alias">
<Form.Item label={t("userEditView.userAliasLabel")} name="user_alias">
<TextInput />
</Form.Item>
<Form.Item
label={
<span>
Global Proxy Role{" "}
<Tooltip title="This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.">
{t("userEditView.globalProxyRoleLabel")}{" "}
<Tooltip title={t("userEditView.globalProxyRoleTooltip")}>
<InfoCircleOutlined />
</Tooltip>
</span>
@ -126,8 +128,8 @@ export function UserEditView({
<Form.Item
label={
<span>
Personal Models{" "}
<Tooltip title="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("userEditView.personalModelsLabel")}{" "}
<Tooltip title={t("userEditView.personalModelsTooltip")}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
@ -136,15 +138,15 @@ export function UserEditView({
>
<Select
mode="multiple"
placeholder="Select models"
placeholder={t("userEditView.selectModelsPlaceholder")}
style={{ width: "100%" }}
disabled={!all_admin_roles.includes(userRole || "")}
>
<Select.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
{t("userEditView.allProxyModels")}
</Select.Option>
<Select.Option key="no-default-models" value="no-default-models">
No Default Models
{t("userEditView.noDefaultModels")}
</Select.Option>
{userModels.map((model) => (
<Select.Option key={model} value={model}>
@ -157,9 +159,9 @@ export function UserEditView({
<Form.Item
label={
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<span>Max Budget (USD)</span>
<span>{t("userEditView.maxBudgetLabel")}</span>
<Checkbox checked={unlimitedBudget} onChange={handleUnlimitedBudgetChange}>
Unlimited Budget
{t("userEditView.unlimitedBudget")}
</Checkbox>
</div>
}
@ -168,7 +170,7 @@ export function UserEditView({
{
validator: (_, value) => {
if (!unlimitedBudget && (value === "" || value === null || value === undefined)) {
return Promise.reject(new Error("Please enter a budget or select Unlimited Budget"));
return Promise.reject(new Error(t("userEditView.budgetValidationMessage")));
}
return Promise.resolve();
},
@ -178,19 +180,19 @@ export function UserEditView({
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} disabled={unlimitedBudget} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Form.Item label={t("userEditView.resetBudgetLabel")} name="budget_duration">
<BudgetDurationDropdown />
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Textarea rows={4} placeholder="Enter metadata as JSON" />
<Form.Item label={t("userEditView.metadataLabel")} name="metadata">
<Textarea rows={4} placeholder={t("userEditView.metadataPlaceholder")} />
</Form.Item>
<div className="flex justify-end space-x-2">
<Button variant="secondary" type="button" onClick={onCancel}>
Cancel
{t("common.cancel")}
</Button>
<Button type="submit">Save Changes</Button>
<Button type="submit">{t("userEditView.saveChangesButton")}</Button>
</div>
</Form>
);

View file

@ -1,5 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { modelAvailableCall } from "./networking";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
@ -16,6 +17,7 @@ interface ViewUserSpendProps {
selectedTeam: any | null;
}
const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget, selectedTeam }) => {
const { t } = useTranslation();
const { accessToken, userRole, userId: userID } = useAuthorized();
let [spend, setSpend] = useState(userSpend !== null ? userSpend : 0.0);
const [maxBudget, setMaxBudget] = useState(
@ -114,7 +116,10 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget,
modelsToDisplay = userModels;
}
const displayMaxBudget = maxBudget !== null ? `$${formatNumberWithCommas(Number(maxBudget), 4)} limit` : "No limit";
const displayMaxBudget =
maxBudget !== null
? t("viewUserSpend.budgetLimit", { amount: formatNumberWithCommas(Number(maxBudget), 4) })
: t("viewUserSpend.noLimit");
const roundedSpend = spend !== undefined ? formatNumberWithCommas(spend, 4) : null;
@ -123,13 +128,17 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget,
<div className="flex items-center">
<div className="flex justify-between gap-x-6">
<div>
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">Total Spend</p>
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">
{t("viewUserSpend.totalSpend")}
</p>
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
${roundedSpend}
</p>
</div>
<div>
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">Max Budget</p>
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">
{t("viewUserSpend.maxBudget")}
</p>
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
{displayMaxBudget}
</p>

View file

@ -1,5 +1,6 @@
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "antd";
import BulkEditUserModal from "./BulkEditUsers";
@ -78,6 +79,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
teams,
orgAdminOrgIds,
}) => {
const { t } = useTranslation();
const isProxyAdmin = userRole ? isProxyAdminRole(userRole) : false;
const queryClient = useQueryClient();
const [currentPage, setCurrentPage] = useState(1);
@ -146,16 +148,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const handleResetPassword = async (userId: string) => {
if (!accessToken) {
NotificationsManager.fromBackend("Access token not found");
NotificationsManager.fromBackend(t("viewUsers.notifications.accessTokenNotFound"));
return;
}
try {
NotificationsManager.success("Generating password reset link...");
NotificationsManager.success(t("viewUsers.notifications.generatingPasswordResetLink"));
const data = await invitationCreateCall(accessToken, userId);
setInvitationLinkData(data);
setIsInvitationLinkModalVisible(true);
} catch (error) {
NotificationsManager.fromBackend("Failed to generate password reset link");
NotificationsManager.fromBackend(t("viewUsers.notifications.failedGeneratePasswordReset"));
}
};
@ -172,10 +174,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
return { ...previousData, users: updatedUsers };
});
NotificationsManager.success("User deleted successfully");
NotificationsManager.success(t("viewUsers.notifications.userDeletedSuccess"));
} catch (error) {
console.error("Error deleting user:", error);
NotificationsManager.fromBackend("Failed to delete user");
NotificationsManager.fromBackend(t("viewUsers.notifications.failedDeleteUser"));
} finally {
setIsDeleteModalOpen(false);
setUserToDelete(null);
@ -215,7 +217,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
return { ...previousData, users: updatedUsers };
});
NotificationsManager.success(`User ${editedUser.user_id} updated successfully`);
NotificationsManager.success(t("viewUsers.notifications.userUpdatedSuccess", { userId: editedUser.user_id }));
} catch (error) {
console.error("There was an error updating the user", error);
}
@ -239,7 +241,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const handleBulkEdit = () => {
if (selectedUsers.length === 0) {
NotificationsManager.fromBackend("Please select users to edit");
NotificationsManager.fromBackend(t("viewUsers.notifications.selectUsersToEdit"));
return;
}
@ -326,7 +328,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
type={selectionMode ? "primary" : "default"}
className="flex items-center"
>
{selectionMode ? "Cancel Selection" : "Select Users"}
{selectionMode ? t("viewUsers.cancelSelection") : t("viewUsers.selectUsers")}
</Button>
)}
@ -337,7 +339,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
disabled={selectedUsers.length === 0}
className="flex items-center"
>
Bulk Edit ({selectedUsers.length} selected)
{t("viewUsers.bulkEdit", { count: selectedUsers.length })}
</Button>
)}
</>
@ -348,8 +350,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
{isProxyAdmin ? (
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "users" : "settings")}>
<TabList className="mb-4">
<Tab>Users</Tab>
<Tab>Default User Settings</Tab>
<Tab>{t("viewUsers.tabUsers")}</Tab>
<Tab>{t("viewUsers.tabDefaultUserSettings")}</Tab>
</TabList>
<TabPanels>
@ -444,18 +446,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete User?"
message="Are you sure you want to delete this user? This action cannot be undone."
resourceInformationTitle="User Information"
title={t("viewUsers.deleteModal.title")}
message={t("viewUsers.deleteModal.message")}
resourceInformationTitle={t("viewUsers.deleteModal.resourceInfoTitle")}
resourceInformation={[
{ label: "Email", value: userToDelete?.user_email },
{ label: "User ID", value: userToDelete?.user_id, code: true },
{ label: t("viewUsers.deleteModal.labelEmail"), value: userToDelete?.user_email },
{ label: t("viewUsers.deleteModal.labelUserId"), value: userToDelete?.user_id, code: true },
{
label: "Global Proxy Role",
label: t("viewUsers.deleteModal.labelGlobalProxyRole"),
value:
(userToDelete && possibleUIRoles?.[userToDelete.user_role]?.ui_label) || userToDelete?.user_role || "-",
},
{ label: "Total Spend (USD)", value: userToDelete?.spend?.toFixed(2) },
{ label: t("viewUsers.deleteModal.labelTotalSpend"), value: userToDelete?.spend?.toFixed(2) },
]}
onCancel={cancelDelete}
onOk={confirmDelete}

View file

@ -1897,6 +1897,59 @@
"spendPerTag": "Spend Per Tag",
"spendPerTagDesc": "Get Started by Tracking cost per tag <docsLink>here</docsLink>"
},
"userDashboard": {
"userIdNotSet": "User ID is not set"
},
"userEditView": {
"userIdLabel": "User ID",
"emailLabel": "Email",
"userAliasLabel": "User Alias",
"globalProxyRoleLabel": "Global Proxy Role",
"globalProxyRoleTooltip": "This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.",
"personalModelsLabel": "Personal Models",
"personalModelsTooltip": "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.",
"selectModelsPlaceholder": "Select models",
"allProxyModels": "All Proxy Models",
"noDefaultModels": "No Default Models",
"maxBudgetLabel": "Max Budget (USD)",
"unlimitedBudget": "Unlimited Budget",
"budgetValidationMessage": "Please enter a budget or select Unlimited Budget",
"resetBudgetLabel": "Reset Budget",
"metadataLabel": "Metadata",
"metadataPlaceholder": "Enter metadata as JSON",
"saveChangesButton": "Save Changes"
},
"viewUserSpend": {
"totalSpend": "Total Spend",
"maxBudget": "Max Budget",
"budgetLimit": "${{amount}} limit",
"noLimit": "No limit"
},
"viewUsers": {
"selectUsers": "Select Users",
"cancelSelection": "Cancel Selection",
"bulkEdit": "Bulk Edit ({{count}} selected)",
"tabUsers": "Users",
"tabDefaultUserSettings": "Default User Settings",
"notifications": {
"accessTokenNotFound": "Access token not found",
"generatingPasswordResetLink": "Generating password reset link...",
"failedGeneratePasswordReset": "Failed to generate password reset link",
"userDeletedSuccess": "User deleted successfully",
"failedDeleteUser": "Failed to delete user",
"selectUsersToEdit": "Please select users to edit",
"userUpdatedSuccess": "User {{userId}} updated successfully"
},
"deleteModal": {
"title": "Delete User?",
"message": "Are you sure you want to delete this user? This action cannot be undone.",
"resourceInfoTitle": "User Information",
"labelEmail": "Email",
"labelUserId": "User ID",
"labelGlobalProxyRole": "Global Proxy Role",
"labelTotalSpend": "Total Spend (USD)"
}
},
"userAgentActivity": {
"summaryTitle": "Summary by User Agent",
"summarySubtitle": "Performance metrics for different user agents",

View file

@ -1897,6 +1897,59 @@
"spendPerTag": "按标签花费",
"spendPerTagDesc": "从按标签追踪成本开始 <docsLink>此处</docsLink>"
},
"userDashboard": {
"userIdNotSet": "用户 ID 未设置"
},
"userEditView": {
"userIdLabel": "用户 ID",
"emailLabel": "邮箱",
"userAliasLabel": "用户别名",
"globalProxyRoleLabel": "全局代理角色",
"globalProxyRoleTooltip": "这是用户在代理上的全局角色,与任何团队/组织特定角色无关。",
"personalModelsLabel": "个人模型",
"personalModelsTooltip": "选择此用户在团队范围外可访问的模型。选择「所有代理模型」以授予对代理上所有可用模型的访问权限。",
"selectModelsPlaceholder": "选择模型",
"allProxyModels": "所有代理模型",
"noDefaultModels": "无默认模型",
"maxBudgetLabel": "最大预算(USD)",
"unlimitedBudget": "无限预算",
"budgetValidationMessage": "请输入预算或选择无限预算",
"resetBudgetLabel": "重置预算",
"metadataLabel": "元数据",
"metadataPlaceholder": "以 JSON 格式输入元数据",
"saveChangesButton": "保存更改"
},
"viewUserSpend": {
"totalSpend": "总花费",
"maxBudget": "最大预算",
"budgetLimit": "${{amount}} 上限",
"noLimit": "无上限"
},
"viewUsers": {
"selectUsers": "选择用户",
"cancelSelection": "取消选择",
"bulkEdit": "批量编辑(已选 {{count}} 个)",
"tabUsers": "用户",
"tabDefaultUserSettings": "默认用户设置",
"notifications": {
"accessTokenNotFound": "未找到访问令牌",
"generatingPasswordResetLink": "正在生成密码重置链接...",
"failedGeneratePasswordReset": "生成密码重置链接失败",
"userDeletedSuccess": "用户已成功删除",
"failedDeleteUser": "删除用户失败",
"selectUsersToEdit": "请选择要编辑的用户",
"userUpdatedSuccess": "用户 {{userId}} 更新成功"
},
"deleteModal": {
"title": "删除用户?",
"message": "确定要删除此用户吗?此操作无法撤销。",
"resourceInfoTitle": "用户信息",
"labelEmail": "邮箱",
"labelUserId": "用户 ID",
"labelGlobalProxyRole": "全局代理角色",
"labelTotalSpend": "总花费(USD)"
}
},
"userAgentActivity": {
"summaryTitle": "按 User Agent 汇总",
"summarySubtitle": "不同 User Agent 的性能指标",