feat(ui): i18n for common_components batch 1

This commit is contained in:
吹雪 2026-06-10 11:52:00 +09:00
parent 019dfcbe23
commit 77fbc36e94
12 changed files with 310 additions and 86 deletions

View file

@ -2,6 +2,7 @@ import React from "react";
import { Select, Skeleton } from "antd";
import { TeamOutlined } from "@ant-design/icons";
import { Text } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { useAccessGroups, AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
export interface AccessGroupSelectorProps {
@ -28,14 +29,15 @@ export interface AccessGroupSelectorProps {
const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
value,
onChange,
placeholder = "Select access groups",
placeholder,
disabled = false,
style,
className,
showLabel = false,
labelText = "Access Group",
labelText,
allowClear = true,
}) => {
const { t } = useTranslation();
const { data: accessGroups, isLoading, isError } = useAccessGroups();
// ── Loading skeleton ─────────────────────────────────────────────────────
@ -44,7 +46,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
<div>
{showLabel && (
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<TeamOutlined className="mr-2" /> {labelText}
<TeamOutlined className="mr-2" /> {labelText ?? t("commonComponents.accessGroupSelector.labelText")}
</Text>
)}
<Skeleton.Input active block style={{ height: 32, ...style }} />
@ -70,13 +72,13 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
<div>
{showLabel && (
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<TeamOutlined className="mr-2" /> {labelText}
<TeamOutlined className="mr-2" /> {labelText ?? t("commonComponents.accessGroupSelector.labelText")}
</Text>
)}
<Select
mode="multiple"
value={value}
placeholder={placeholder}
placeholder={placeholder ?? t("commonComponents.accessGroupSelector.placeholder")}
onChange={onChange}
disabled={disabled}
allowClear={allowClear}
@ -84,7 +86,11 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
style={{ width: "100%", ...style }}
className={`rounded-md ${className ?? ""}`}
notFoundContent={
isError ? <span className="text-red-500">Failed to load access groups</span> : "No access groups found"
isError ? (
<span className="text-red-500">{t("commonComponents.accessGroupSelector.loadFailed")}</span>
) : (
t("commonComponents.accessGroupSelector.notFound")
)
}
filterOption={(input, option) => {
const searchText = options.find((opt) => opt.value === option?.value)?.searchText ?? "";

View file

@ -1,6 +1,7 @@
import React from "react";
import { Text, Badge } from "@tremor/react";
import { RefreshIcon, ClockIcon } from "@heroicons/react/outline";
import { useTranslation } from "react-i18next";
interface AutoRotationViewProps {
autoRotate?: boolean;
@ -21,6 +22,8 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
variant = "card",
className = "",
}) => {
const { t } = useTranslation();
const formatTimestamp = (timestamp: string | Date) => {
const date = new Date(timestamp);
const dateStr = date.toLocaleDateString("en-US", {
@ -42,14 +45,16 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
<div className="space-y-3">
<div className="flex items-center gap-2">
<RefreshIcon className="h-4 w-4 text-blue-600" />
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
<Text className="font-semibold text-gray-900">{t("commonComponents.autoRotationView.title")}</Text>
<Badge color={autoRotate ? "green" : "gray"} size="xs">
{autoRotate ? "Enabled" : "Disabled"}
{autoRotate ? t("common.enabled") : t("common.disabled")}
</Badge>
{autoRotate && rotationInterval && (
<>
<Text className="text-gray-400">•</Text>
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
<Text className="text-sm text-gray-600">
{t("commonComponents.autoRotationView.every", { interval: rotationInterval })}
</Text>
</>
)}
</div>
@ -63,7 +68,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="flex-1">
<Text className="font-medium text-gray-700">Last Rotation</Text>
<Text className="font-medium text-gray-700">{t("commonComponents.autoRotationView.lastRotation")}</Text>
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
</div>
</div>
@ -74,7 +79,9 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="flex-1">
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
<Text className="font-medium text-gray-700">
{t("commonComponents.autoRotationView.nextScheduledRotation")}
</Text>
<Text className="text-sm text-gray-600">{formatTimestamp(nextRotationAt || keyRotationAt || "")}</Text>
</div>
</div>
@ -84,7 +91,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
<ClockIcon className="w-4 h-4 text-gray-500" />
<Text className="text-gray-600">No rotation history available</Text>
<Text className="text-gray-600">{t("commonComponents.autoRotationView.noRotationHistory")}</Text>
</div>
)}
</div>
@ -94,7 +101,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
<RefreshIcon className="w-4 h-4 text-gray-400" />
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
<Text className="text-gray-600">{t("commonComponents.autoRotationView.notEnabled")}</Text>
</div>
)}
</div>
@ -105,8 +112,8 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
<Text className="text-xs text-gray-500">Automatic key rotation settings and status for this key</Text>
<Text className="font-semibold text-gray-900">{t("commonComponents.autoRotationView.title")}</Text>
<Text className="text-xs text-gray-500">{t("commonComponents.autoRotationView.subtitle")}</Text>
</div>
</div>
{content}
@ -116,7 +123,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
return (
<div className={`${className}`}>
<Text className="font-medium text-gray-900 mb-3">Auto-Rotation</Text>
<Text className="font-medium text-gray-900 mb-3">{t("commonComponents.autoRotationView.title")}</Text>
{content}
</div>
);

View file

@ -1,4 +1,5 @@
import { Tag, Typography } from "antd";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
@ -14,8 +15,10 @@ interface DefaultProxyAdminTagProps {
* plain text.
*/
export default function DefaultProxyAdminTag({ userId }: DefaultProxyAdminTagProps) {
const { t } = useTranslation();
if (userId === DEFAULT_USER_ID) {
return <Tag color="blue">Default Proxy Admin</Tag>;
return <Tag color="blue">{t("commonComponents.defaultProxyAdminTag.label")}</Tag>;
}
return <Text>{userId}</Text>;

View file

@ -1,6 +1,7 @@
import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import React, { useState, useEffect } from "react";
import { Trans, useTranslation } from "react-i18next";
interface DeleteResourceModalProps {
isOpen: boolean;
@ -32,6 +33,7 @@ export default function DeleteResourceModal({
confirmLoading,
requiredConfirmation,
}: DeleteResourceModalProps) {
const { t } = useTranslation();
const { Title, Text } = Typography;
const { token } = theme.useToken();
const [requiredConfirmationInput, setRequiredConfirmationInput] = useState("");
@ -49,8 +51,8 @@ export default function DeleteResourceModal({
onOk={onOk}
onCancel={onCancel}
confirmLoading={confirmLoading}
okText={confirmLoading ? "Deleting..." : "Delete"}
cancelText="Cancel"
okText={confirmLoading ? t("common.deleting") : t("commonComponents.deleteResourceModal.okText")}
cancelText={t("common.cancel")}
okButtonProps={{
danger: true,
disabled: (!!requiredConfirmation && requiredConfirmationInput !== requiredConfirmation) || confirmLoading,
@ -89,11 +91,13 @@ export default function DeleteResourceModal({
{requiredConfirmation && (
<div className="mb-6 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<Text className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
<Text>Type </Text>
<Text strong type="danger">
{requiredConfirmation}
</Text>
<Text> to confirm deletion:</Text>
<Trans
i18nKey="commonComponents.deleteResourceModal.typeToConfirm"
values={{ name: requiredConfirmation }}
components={{
danger: <Text strong type="danger" />,
}}
/>
</Text>
<Input
value={requiredConfirmationInput}

View file

@ -1,4 +1,5 @@
import { Select } from "antd";
import { useTranslation } from "react-i18next";
interface DurationSelectProps {
className?: string;
@ -7,11 +8,13 @@ interface DurationSelectProps {
}
export default function DurationSelect({ className, value, onChange }: DurationSelectProps) {
const { t } = useTranslation();
return (
<Select className={className} value={value} onChange={onChange}>
<Select.Option value="24h">Daily</Select.Option>
<Select.Option value="7d">Weekly</Select.Option>
<Select.Option value="30d">Monthly</Select.Option>
<Select.Option value="24h">{t("commonComponents.durationSelect.daily")}</Select.Option>
<Select.Option value="7d">{t("commonComponents.durationSelect.weekly")}</Select.Option>
<Select.Option value="30d">{t("commonComponents.durationSelect.monthly")}</Select.Option>
</Select>
);
}

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Select, Tooltip, Divider, Switch, Checkbox } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
@ -26,6 +27,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
neverExpire = false,
onNeverExpireChange,
}) => {
const { t } = useTranslation();
// Predefined intervals
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
@ -65,12 +68,14 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
<div className="space-y-6">
{/* Key Expiry Section */}
<div className="space-y-4">
<span className="text-sm font-medium text-gray-700">Key Expiry Settings</span>
<span className="text-sm font-medium text-gray-700">
{t("commonComponents.keyLifecycleSettings.keyExpirySectionTitle")}
</span>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
<span>Expire Key</span>
<Tooltip title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.">
<span>{t("commonComponents.keyLifecycleSettings.expireKeyLabel")}</span>
<Tooltip title={t("commonComponents.keyLifecycleSettings.expireKeyTooltip")}>
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
</Tooltip>
{!isCreateMode && onNeverExpireChange && (
@ -90,13 +95,17 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
}}
className="ml-2 text-sm font-normal text-gray-600"
>
Never Expire
{t("commonComponents.keyLifecycleSettings.neverExpire")}
</Checkbox>
)}
</label>
<TextInput
name="duration"
placeholder={isCreateMode ? "e.g., 30d or leave empty to never expire" : "e.g., 30d"}
placeholder={
isCreateMode
? t("commonComponents.keyLifecycleSettings.expireKeyPlaceholderCreate")
: t("commonComponents.keyLifecycleSettings.expireKeyPlaceholderEdit")
}
className="w-full"
value={durationValue}
onValueChange={handleDurationChange}
@ -109,13 +118,15 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
{/* Auto-Rotation Section */}
<div className="space-y-4">
<span className="text-sm font-medium text-gray-700">Auto-Rotation Settings</span>
<span className="text-sm font-medium text-gray-700">
{t("commonComponents.keyLifecycleSettings.autoRotationSectionTitle")}
</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
<span>Enable Auto-Rotation</span>
<Tooltip title="Key will automatically regenerate at the specified interval for enhanced security.">
<span>{t("commonComponents.keyLifecycleSettings.enableAutoRotationLabel")}</span>
<Tooltip title={t("commonComponents.keyLifecycleSettings.enableAutoRotationTooltip")}>
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
</Tooltip>
</label>
@ -130,8 +141,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
{autoRotationEnabled && (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
<span>Rotation Interval</span>
<Tooltip title="How often the key should be automatically rotated. Choose the interval that best fits your security requirements.">
<span>{t("commonComponents.keyLifecycleSettings.rotationIntervalLabel")}</span>
<Tooltip title={t("commonComponents.keyLifecycleSettings.rotationIntervalTooltip")}>
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
</Tooltip>
</label>
@ -140,14 +151,14 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
value={showCustomInput ? "custom" : rotationInterval}
onChange={handleIntervalChange}
className="w-full"
placeholder="Select interval"
placeholder={t("commonComponents.keyLifecycleSettings.selectInterval")}
>
<Option value="7d">7 days</Option>
<Option value="30d">30 days</Option>
<Option value="90d">90 days</Option>
<Option value="180d">180 days</Option>
<Option value="365d">365 days</Option>
<Option value="custom">Custom interval</Option>
<Option value="7d">{t("commonComponents.keyLifecycleSettings.sevenDays")}</Option>
<Option value="30d">{t("commonComponents.keyLifecycleSettings.thirtyDays")}</Option>
<Option value="90d">{t("commonComponents.keyLifecycleSettings.ninetyDays")}</Option>
<Option value="180d">{t("commonComponents.keyLifecycleSettings.oneHundredEightyDays")}</Option>
<Option value="365d">{t("commonComponents.keyLifecycleSettings.threeHundredSixtyFiveDays")}</Option>
<Option value="custom">{t("commonComponents.keyLifecycleSettings.customInterval")}</Option>
</Select>
{showCustomInput && (
@ -155,10 +166,10 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
<TextInput
value={customInterval}
onChange={handleCustomIntervalChange}
placeholder="e.g., 1s, 5m, 2h, 14d"
placeholder={t("commonComponents.keyLifecycleSettings.customIntervalPlaceholder")}
/>
<div className="text-xs text-gray-500">
Supported formats: seconds (s), minutes (m), hours (h), days (d)
{t("commonComponents.keyLifecycleSettings.customIntervalFormats")}
</div>
</div>
)}
@ -169,8 +180,7 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
{autoRotationEnabled && (
<div className="bg-blue-50 p-3 rounded-md text-sm text-blue-700">
When rotation occurs, you&apos;ll receive a notification with the new key. The old key will be deactivated
after a brief grace period.
{t("commonComponents.keyLifecycleSettings.rotationNotice")}
</div>
)}
</div>

View file

@ -1,14 +1,17 @@
import { cx } from "@/lib/cva.config";
import { UiLoadingSpinner } from "../ui/ui-loading-spinner";
import { useTranslation } from "react-i18next";
export default function LoadingScreen() {
const { t } = useTranslation();
return (
<div className={cx("h-screen", "flex items-center justify-center gap-4")}>
<div className="text-lg font-medium py-2 pr-4 border-r border-r-gray-200">🚅 LiteLLM</div>
<div className="flex items-center justify-center gap-2">
<UiLoadingSpinner className="size-4" />
<span className="text-gray-600 text-sm">Loading...</span>
<span className="text-gray-600 text-sm">{t("common.loading")}</span>
</div>
</div>
);

View file

@ -3,6 +3,7 @@ import { CrownOutlined, InfoCircleOutlined, UserAddOutlined, UserOutlined } from
import { Button, Space, Table, Tag, Tooltip, Typography } from "antd";
import type { ColumnsType } from "antd/es/table";
import React from "react";
import { useTranslation } from "react-i18next";
import TableIconActionButton from "./IconActionButton/TableIconActionButtons/TableIconActionButton";
const { Text } = Typography;
@ -26,36 +27,41 @@ export default function MemberTable({
onEdit,
onDelete,
onAddMember,
roleColumnTitle = "Role",
roleColumnTitle,
roleTooltip,
extraColumns = [],
showDeleteForMember,
emptyText,
}: MemberTableProps) {
const { t } = useTranslation();
const baseColumns: ColumnsType<Member> = [
{
title: "User Email",
title: t("commonComponents.memberTable.userEmail"),
dataIndex: "user_email",
key: "user_email",
render: (email: string | null) => <Text>{email || "-"}</Text>,
},
{
title: "User ID",
title: t("commonComponents.memberTable.userId"),
dataIndex: "user_id",
key: "user_id",
render: (userId: string | null) =>
userId === "default_user_id" ? <Tag color="blue">Default Proxy Admin</Tag> : <Text>{userId || "-"}</Text>,
userId === "default_user_id" ? (
<Tag color="blue">{t("commonComponents.memberTable.defaultProxyAdmin")}</Tag>
) : (
<Text>{userId || "-"}</Text>
),
},
{
title: roleTooltip ? (
<Space direction="horizontal">
{roleColumnTitle}
{roleColumnTitle ?? t("user.role")}
<Tooltip title={roleTooltip}>
<InfoCircleOutlined />
</Tooltip>
</Space>
) : (
roleColumnTitle
roleColumnTitle ?? t("user.role")
),
dataIndex: "role",
key: "role",
@ -72,7 +78,7 @@ export default function MemberTable({
},
...extraColumns,
{
title: "Actions",
title: t("common.actions"),
key: "actions",
fixed: "right" as const,
width: 120,
@ -81,14 +87,14 @@ export default function MemberTable({
<Space>
<TableIconActionButton
variant="Edit"
tooltipText="Edit member"
tooltipText={t("commonComponents.memberTable.editMember")}
dataTestId="edit-member"
onClick={() => onEdit(record)}
/>
{(!showDeleteForMember || showDeleteForMember(record)) && (
<TableIconActionButton
variant="Delete"
tooltipText="Delete member"
tooltipText={t("commonComponents.memberTable.deleteMember")}
dataTestId="delete-member"
onClick={() => onDelete(record)}
/>
@ -101,7 +107,7 @@ export default function MemberTable({
return (
<Space direction="vertical" style={{ width: "100%" }}>
<span className="inline-flex text-sm text-gray-700">
{members.length} Member{members.length !== 1 ? "s" : ""}
{t("commonComponents.memberTable.memberCount", { count: members.length })}
</span>
<Table
columns={baseColumns}
@ -114,7 +120,7 @@ export default function MemberTable({
/>
{onAddMember && canEdit && (
<Button icon={<UserAddOutlined />} type="primary" onClick={onAddMember}>
Add Member
{t("commonComponents.memberTable.addMember")}
</Button>
)}
</Space>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline";
import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { useTranslation } from "react-i18next";
import ModelSelector from "./ModelSelector";
import NotificationsManager from "../molecules/notifications_manager";
@ -23,6 +24,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
onAliasUpdate,
showExampleConfig = true,
}) => {
const { t } = useTranslation();
const [aliases, setAliases] = useState<AliasItem[]>([]);
const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" });
const [editingAlias, setEditingAlias] = useState<AliasItem | null>(null);
@ -39,13 +41,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
const handleAddAlias = () => {
if (!newAlias.aliasName || !newAlias.targetModel) {
NotificationsManager.fromBackend("Please provide both alias name and target model");
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.bothRequired"));
return;
}
// Check for duplicate alias names
if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) {
NotificationsManager.fromBackend("An alias with this name already exists");
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.duplicateAlias"));
return;
}
@ -69,7 +71,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
onAliasUpdate(aliasObject);
}
NotificationsManager.success("Alias added successfully");
NotificationsManager.success(t("commonComponents.modelAliasManager.addSuccess"));
};
const handleEditAlias = (alias: AliasItem) => {
@ -80,13 +82,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
if (!editingAlias) return;
if (!editingAlias.aliasName || !editingAlias.targetModel) {
NotificationsManager.fromBackend("Please provide both alias name and target model");
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.bothRequired"));
return;
}
// Check for duplicate alias names (excluding current alias)
if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) {
NotificationsManager.fromBackend("An alias with this name already exists");
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.duplicateAlias"));
return;
}
@ -105,7 +107,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
onAliasUpdate(aliasObject);
}
NotificationsManager.success("Alias updated successfully");
NotificationsManager.success(t("commonComponents.modelAliasManager.updateSuccess"));
};
const handleCancelEdit = () => {
@ -126,7 +128,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
onAliasUpdate(aliasObject);
}
NotificationsManager.success("Alias deleted successfully");
NotificationsManager.success(t("commonComponents.modelAliasManager.deleteSuccess"));
};
// Convert current aliases to object for config example
@ -141,10 +143,14 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
return (
<div className="mt-4">
<div className="mb-6">
<Text className="text-sm font-medium text-gray-700 mb-2">Add New Alias</Text>
<Text className="text-sm font-medium text-gray-700 mb-2">
{t("commonComponents.modelAliasManager.addNewAlias")}
</Text>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">Alias Name</label>
<label className="block text-xs text-gray-500 mb-1">
{t("commonComponents.modelAliasManager.aliasName")}
</label>
<input
type="text"
value={newAlias.aliasName}
@ -159,7 +165,9 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Target Model</label>
<label className="block text-xs text-gray-500 mb-1">
{t("commonComponents.modelAliasManager.targetModel")}
</label>
<ModelSelector
accessToken={accessToken}
value={newAlias.targetModel}
@ -180,21 +188,27 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700"}`}
>
<PlusCircleIcon className="w-4 h-4 mr-1" />
Add Alias
{t("commonComponents.modelAliasManager.addAlias")}
</button>
</div>
</div>
</div>
<Text className="text-sm font-medium text-gray-700 mb-2">Manage Existing Aliases</Text>
<Text className="text-sm font-medium text-gray-700 mb-2">
{t("commonComponents.modelAliasManager.manageExisting")}
</Text>
<div className="rounded-lg custom-border relative mb-6">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
<TableRow>
<TableHeaderCell className="py-1 h-8">Alias Name</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">Target Model</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
{t("commonComponents.modelAliasManager.aliasName")}
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
{t("commonComponents.modelAliasManager.targetModel")}
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">{t("common.actions")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -235,13 +249,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
onClick={handleUpdateAlias}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
>
Save
{t("common.save")}
</button>
<button
onClick={handleCancelEdit}
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100"
>
Cancel
{t("common.cancel")}
</button>
</div>
</TableCell>
@ -273,7 +287,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{aliases.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
No aliases added yet. Add a new alias above.
{t("commonComponents.modelAliasManager.noAliases")}
</TableCell>
</TableRow>
)}
@ -285,8 +299,8 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{/* Configuration Example */}
{showExampleConfig && (
<Card>
<Title className="mb-4">Configuration Example</Title>
<Text className="text-gray-600 mb-4">Here&apos;s how your current aliases would look in the config:</Text>
<Title className="mb-4">{t("commonComponents.modelAliasManager.configExample")}</Title>
<Text className="text-gray-600 mb-4">{t("commonComponents.modelAliasManager.configExampleDesc")}</Text>
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-700">
model_aliases:

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { TextInput, Text } from "@tremor/react";
import { Select } from "antd";
import { RobotOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
interface ModelSelectorProps {
@ -19,14 +20,15 @@ interface ModelSelectorProps {
const ModelSelector: React.FC<ModelSelectorProps> = ({
accessToken,
value,
placeholder = "Select a Model",
placeholder,
onChange,
disabled = false,
style,
className,
showLabel = true,
labelText = "Select Model",
labelText,
}) => {
const { t } = useTranslation();
const [selectedModel, setSelectedModel] = useState<string | undefined>(value);
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
@ -86,12 +88,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
<div>
{showLabel && (
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<RobotOutlined className="mr-2" /> {labelText}
<RobotOutlined className="mr-2" /> {labelText ?? t("commonComponents.modelSelector.labelText")}
</Text>
)}
<Select
value={selectedModel}
placeholder={placeholder}
placeholder={placeholder ?? t("commonComponents.modelSelector.placeholder")}
onChange={onModelChange}
options={[
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group, index) => ({
@ -99,7 +101,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
label: model_group,
key: index,
})),
{ value: "custom", label: "Enter custom model", key: "custom" },
{ value: "custom", label: t("commonComponents.modelSelector.enterCustomModel"), key: "custom" },
]}
style={{ width: "100%", ...style }}
showSearch={true}
@ -109,7 +111,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
{showCustomModelInput && (
<TextInput
className="mt-2"
placeholder="Enter custom model name"
placeholder={t("commonComponents.modelSelector.customModelPlaceholder")}
onValueChange={handleCustomModelChange}
disabled={disabled}
/>

View file

@ -197,6 +197,89 @@
"rateLimit": "Rate Limit"
}
},
"commonComponents": {
"accessGroupSelector": {
"placeholder": "Select access groups",
"labelText": "Access Group",
"loadFailed": "Failed to load access groups",
"notFound": "No access groups found"
},
"autoRotationView": {
"title": "Auto-Rotation",
"subtitle": "Automatic key rotation settings and status for this key",
"every": "Every {{interval}}",
"lastRotation": "Last Rotation",
"nextScheduledRotation": "Next Scheduled Rotation",
"noRotationHistory": "No rotation history available",
"notEnabled": "Auto-rotation is not enabled for this key"
},
"defaultProxyAdminTag": {
"label": "Default Proxy Admin"
},
"deleteResourceModal": {
"okText": "Delete",
"typeToConfirm": "Type <danger>{{name}}</danger> to confirm deletion:"
},
"durationSelect": {
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"keyLifecycleSettings": {
"keyExpirySectionTitle": "Key Expiry Settings",
"expireKeyLabel": "Expire Key",
"expireKeyTooltip": "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.",
"neverExpire": "Never Expire",
"expireKeyPlaceholderCreate": "e.g., 30d or leave empty to never expire",
"expireKeyPlaceholderEdit": "e.g., 30d",
"autoRotationSectionTitle": "Auto-Rotation Settings",
"enableAutoRotationLabel": "Enable Auto-Rotation",
"enableAutoRotationTooltip": "Key will automatically regenerate at the specified interval for enhanced security.",
"rotationIntervalLabel": "Rotation Interval",
"rotationIntervalTooltip": "How often the key should be automatically rotated. Choose the interval that best fits your security requirements.",
"selectInterval": "Select interval",
"sevenDays": "7 days",
"thirtyDays": "30 days",
"ninetyDays": "90 days",
"oneHundredEightyDays": "180 days",
"threeHundredSixtyFiveDays": "365 days",
"customInterval": "Custom interval",
"customIntervalPlaceholder": "e.g., 1s, 5m, 2h, 14d",
"customIntervalFormats": "Supported formats: seconds (s), minutes (m), hours (h), days (d)",
"rotationNotice": "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period."
},
"memberTable": {
"userEmail": "User Email",
"userId": "User ID",
"defaultProxyAdmin": "Default Proxy Admin",
"memberCount_one": "{{count}} Member",
"memberCount_other": "{{count}} Members",
"editMember": "Edit member",
"deleteMember": "Delete member",
"addMember": "Add Member"
},
"modelAliasManager": {
"bothRequired": "Please provide both alias name and target model",
"duplicateAlias": "An alias with this name already exists",
"addSuccess": "Alias added successfully",
"updateSuccess": "Alias updated successfully",
"deleteSuccess": "Alias deleted successfully",
"addNewAlias": "Add New Alias",
"aliasName": "Alias Name",
"targetModel": "Target Model",
"addAlias": "Add Alias",
"manageExisting": "Manage Existing Aliases",
"noAliases": "No aliases added yet. Add a new alias above.",
"configExample": "Configuration Example",
"configExampleDesc": "Here's how your current aliases would look in the config:"
},
"modelSelector": {
"placeholder": "Select a Model",
"labelText": "Select Model",
"enterCustomModel": "Enter custom model",
"customModelPlaceholder": "Enter custom model name"
}
},
"common": {
"save": "Save",
"cancel": "Cancel",

View file

@ -197,6 +197,89 @@
"rateLimit": "速率限制"
}
},
"commonComponents": {
"accessGroupSelector": {
"placeholder": "选择访问组",
"labelText": "访问组",
"loadFailed": "加载访问组失败",
"notFound": "未找到访问组"
},
"autoRotationView": {
"title": "自动轮换",
"subtitle": "此密钥的自动轮换设置与状态",
"every": "每 {{interval}}",
"lastRotation": "上次轮换",
"nextScheduledRotation": "下次计划轮换",
"noRotationHistory": "暂无轮换历史",
"notEnabled": "此密钥未启用自动轮换"
},
"defaultProxyAdminTag": {
"label": "默认代理管理员"
},
"deleteResourceModal": {
"okText": "删除",
"typeToConfirm": "请输入 <danger>{{name}}</danger> 以确认删除:"
},
"durationSelect": {
"daily": "每天",
"weekly": "每周",
"monthly": "每月"
},
"keyLifecycleSettings": {
"keyExpirySectionTitle": "密钥过期设置",
"expireKeyLabel": "密钥过期",
"expireKeyTooltip": "设置此密钥的过期时间。格式:30s(秒)、30m(分钟)、30h(小时)、30d(天)。留空则保持当前过期时间不变。",
"neverExpire": "永不过期",
"expireKeyPlaceholderCreate": "例如:30d,留空则永不过期",
"expireKeyPlaceholderEdit": "例如:30d",
"autoRotationSectionTitle": "自动轮换设置",
"enableAutoRotationLabel": "启用自动轮换",
"enableAutoRotationTooltip": "密钥将按指定间隔自动重新生成,以增强安全性。",
"rotationIntervalLabel": "轮换间隔",
"rotationIntervalTooltip": "密钥自动轮换的频率。请选择最适合您安全要求的间隔。",
"selectInterval": "选择间隔",
"sevenDays": "7 天",
"thirtyDays": "30 天",
"ninetyDays": "90 天",
"oneHundredEightyDays": "180 天",
"threeHundredSixtyFiveDays": "365 天",
"customInterval": "自定义间隔",
"customIntervalPlaceholder": "例如:1s、5m、2h、14d",
"customIntervalFormats": "支持格式:秒(s)、分钟(m)、小时(h)、天(d)",
"rotationNotice": "轮换发生时,您将收到包含新密钥的通知。旧密钥将在短暂的宽限期后停用。"
},
"memberTable": {
"userEmail": "用户邮箱",
"userId": "用户 ID",
"defaultProxyAdmin": "默认代理管理员",
"memberCount_one": "{{count}} 位成员",
"memberCount_other": "{{count}} 位成员",
"editMember": "编辑成员",
"deleteMember": "删除成员",
"addMember": "添加成员"
},
"modelAliasManager": {
"bothRequired": "请同时提供别名名称和目标模型",
"duplicateAlias": "同名别名已存在",
"addSuccess": "别名添加成功",
"updateSuccess": "别名更新成功",
"deleteSuccess": "别名删除成功",
"addNewAlias": "添加新别名",
"aliasName": "别名名称",
"targetModel": "目标模型",
"addAlias": "添加别名",
"manageExisting": "管理现有别名",
"noAliases": "暂无别名。请在上方添加新别名。",
"configExample": "配置示例",
"configExampleDesc": "以下是您当前别名在配置中的样子:"
},
"modelSelector": {
"placeholder": "选择模型",
"labelText": "选择模型",
"enterCustomModel": "输入自定义模型",
"customModelPlaceholder": "输入自定义模型名称"
}
},
"common": {
"save": "保存",
"cancel": "取消",