feat(ui): i18n for access groups

This commit is contained in:
吹雪 2026-06-11 04:25:23 +09:00
parent caf77c8419
commit cd547a117b
7 changed files with 206 additions and 62 deletions

View file

@ -17,6 +17,7 @@ import {
} from "antd";
import { ArrowLeftIcon, BotIcon, EditIcon, KeyIcon, LayersIcon, ServerIcon, UsersIcon } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal";
@ -29,6 +30,7 @@ interface AccessGroupDetailProps {
}
export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailProps) {
const { t } = useTranslation();
const { data: accessGroup, isLoading } = useAccessGroupDetails(accessGroupId);
const { token } = theme.useToken();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
@ -61,7 +63,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
}}
>
<Button icon={<ArrowLeftIcon size={16} />} onClick={onBack} type="text" style={{ marginBottom: 16 }} />
<Empty description="Access group not found" />
<Empty description={t("accessGroups.accessGroupsDetailsPage.notFound")} />
</Content>
);
}
@ -85,7 +87,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
label: (
<Flex align="center" gap={8}>
<LayersIcon size={16} />
Models
{t("accessGroups.accessGroupsDetailsPage.tabModels")}
<Tag style={{ marginInlineEnd: 0 }}>{modelIds?.length}</Tag>
</Flex>
),
@ -103,7 +105,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
)}
/>
) : (
<Empty description="No models assigned to this group" />
<Empty description={t("accessGroups.accessGroupsDetailsPage.noModels")} />
),
},
{
@ -111,7 +113,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
label: (
<Flex align="center" gap={8}>
<ServerIcon size={16} />
MCP Servers
{t("accessGroups.accessGroupsDetailsPage.tabMcpServers")}
<Tag>{mcpServerIds?.length}</Tag>
</Flex>
),
@ -129,7 +131,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
)}
/>
) : (
<Empty description="No MCP servers assigned to this group" />
<Empty description={t("accessGroups.accessGroupsDetailsPage.noMcpServers")} />
),
},
{
@ -137,7 +139,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
label: (
<Flex align="center" gap={8}>
<BotIcon size={16} />
Agents
{t("accessGroups.accessGroupsDetailsPage.tabAgents")}
<Tag>{agentIds?.length}</Tag>
</Flex>
),
@ -155,7 +157,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
)}
/>
) : (
<Empty description="No agents assigned to this group" />
<Empty description={t("accessGroups.accessGroupsDetailsPage.noAgents")} />
),
},
];
@ -183,29 +185,29 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
</div>
</div>
<Button type="primary" icon={<EditIcon size={16} />} onClick={handleEdit}>
Edit Access Group
{t("accessGroups.accessGroupsDetailsPage.editButton")}
</Button>
</div>
{/* Group Details */}
<Row style={{ marginBottom: 24 }}>
<Card>
<Descriptions title="Group Details" column={1}>
<Descriptions.Item label="Description">{accessGroup.description || "—"}</Descriptions.Item>
<Descriptions.Item label="Created">
<Descriptions title={t("accessGroups.accessGroupsDetailsPage.groupDetails")} column={1}>
<Descriptions.Item label={t("common.description")}>{accessGroup.description || "—"}</Descriptions.Item>
<Descriptions.Item label={t("accessGroups.accessGroupsDetailsPage.created")}>
{new Date(accessGroup.created_at).toLocaleString()}
{accessGroup.created_by && (
<Text>
&nbsp;{"by"}&nbsp;
&nbsp;{t("accessGroups.accessGroupsDetailsPage.by")}&nbsp;
<DefaultProxyAdminTag userId={accessGroup.created_by} />
</Text>
)}
</Descriptions.Item>
<Descriptions.Item label="Last Updated">
<Descriptions.Item label={t("accessGroups.accessGroupsDetailsPage.lastUpdated")}>
{new Date(accessGroup.updated_at).toLocaleString()}
{accessGroup.updated_by && (
<Text>
&nbsp;{"by"}&nbsp;
&nbsp;{t("accessGroups.accessGroupsDetailsPage.by")}&nbsp;
<DefaultProxyAdminTag userId={accessGroup.updated_by} />
</Text>
)}
@ -221,14 +223,16 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
title={
<Flex align="center" gap={8}>
<KeyIcon size={16} />
Attached Keys
{t("accessGroups.accessGroupsDetailsPage.attachedKeys")}
<Tag>{keyIds?.length}</Tag>
</Flex>
}
extra={
keyIds?.length > MAX_PREVIEW ? (
<Button type="link" onClick={() => setShowAllKeys(!showAllKeys)}>
{showAllKeys ? "Show Less" : `View All (${keyIds?.length})`}
{showAllKeys
? t("accessGroups.accessGroupsDetailsPage.showLess")
: t("accessGroups.accessGroupsDetailsPage.viewAll", { count: keyIds?.length })}
</Button>
) : null
}
@ -244,7 +248,10 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
))}
</Flex>
) : (
<Empty description="No keys attached" image={Empty.PRESENTED_IMAGE_SIMPLE} />
<Empty
description={t("accessGroups.accessGroupsDetailsPage.noKeys")}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Card>
</Col>
@ -253,14 +260,16 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
title={
<Flex align="center" gap={8}>
<UsersIcon size={16} />
Attached Teams
{t("accessGroups.accessGroupsDetailsPage.attachedTeams")}
<Tag>{teamIds?.length}</Tag>
</Flex>
}
extra={
teamIds?.length > MAX_PREVIEW ? (
<Button type="link" onClick={() => setShowAllTeams(!showAllTeams)}>
{showAllTeams ? "Show Less" : `View All (${teamIds?.length})`}
{showAllTeams
? t("accessGroups.accessGroupsDetailsPage.showLess")
: t("accessGroups.accessGroupsDetailsPage.viewAll", { count: teamIds?.length })}
</Button>
) : null
}
@ -276,7 +285,10 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
))}
</Flex>
) : (
<Empty description="No teams attached" image={Empty.PRESENTED_IMAGE_SIMPLE} />
<Empty
description={t("accessGroups.accessGroupsDetailsPage.noTeams")}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Card>
</Col>

View file

@ -4,6 +4,7 @@ import { ModelSelect } from "@/components/ModelSelect/ModelSelect";
import type { FormInstance } from "antd";
import { Form, Input, Select, Space, Tabs } from "antd";
import { BotIcon, InfoIcon, LayersIcon, ServerIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
const { TextArea } = Input;
@ -21,6 +22,7 @@ interface AccessGroupBaseFormProps {
}
export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGroupBaseFormProps) {
const { t } = useTranslation();
const { data: agentsData } = useAgents();
const { data: mcpServersData } = useMCPServers();
@ -32,25 +34,25 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
label: (
<Space align="center" size={4}>
<InfoIcon size={16} />
General Info
{t("accessGroups.accessGroupBaseForm.tabGeneralInfo")}
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item
name="name"
label="Group Name"
label={t("accessGroups.accessGroupBaseForm.labelGroupName")}
rules={[
{
required: true,
message: "Please enter the access group name",
message: t("accessGroups.accessGroupBaseForm.nameRequired"),
},
]}
>
<Input placeholder="e.g. Engineering Team" disabled={isNameDisabled} />
<Input placeholder={t("accessGroups.accessGroupBaseForm.namePlaceholder")} disabled={isNameDisabled} />
</Form.Item>
<Form.Item name="description" label="Description">
<TextArea rows={4} placeholder="Describe the purpose of this access group..." />
<Form.Item name="description" label={t("common.description")}>
<TextArea rows={4} placeholder={t("accessGroups.accessGroupBaseForm.descriptionPlaceholder")} />
</Form.Item>
</div>
),
@ -60,12 +62,12 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
label: (
<Space align="center" size={4}>
<LayersIcon size={16} />
Models
{t("accessGroups.accessGroupBaseForm.tabModels")}
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item name="modelIds" label="Allowed Models">
<Form.Item name="modelIds" label={t("accessGroups.accessGroupBaseForm.labelAllowedModels")}>
<ModelSelect
context="global"
value={form.getFieldValue("modelIds") ?? []}
@ -81,15 +83,15 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
label: (
<Space align="center" size={4}>
<ServerIcon size={16} />
MCP Servers
{t("accessGroups.accessGroupBaseForm.tabMcpServers")}
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item name="mcpServerIds" label="Allowed MCP Servers">
<Form.Item name="mcpServerIds" label={t("accessGroups.accessGroupBaseForm.labelAllowedMcpServers")}>
<Select
mode="multiple"
placeholder="Select MCP servers"
placeholder={t("accessGroups.accessGroupBaseForm.mcpServersPlaceholder")}
style={{ width: "100%" }}
optionFilterProp="label"
allowClear
@ -107,15 +109,15 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
label: (
<Space align="center" size={4}>
<BotIcon size={16} />
Agents
{t("accessGroups.accessGroupBaseForm.tabAgents")}
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item name="agentIds" label="Allowed Agents">
<Form.Item name="agentIds" label={t("accessGroups.accessGroupBaseForm.labelAllowedAgents")}>
<Select
mode="multiple"
placeholder="Select agents"
placeholder={t("accessGroups.accessGroupBaseForm.agentsPlaceholder")}
style={{ width: "100%" }}
optionFilterProp="label"
allowClear

View file

@ -1,5 +1,6 @@
import React from "react";
import { Modal, Form } from "antd";
import { useTranslation } from "react-i18next";
import MessageManager from "@/components/molecules/message_manager";
import { AccessGroupBaseForm, AccessGroupFormValues } from "./AccessGroupBaseForm";
import {
@ -14,6 +15,7 @@ interface AccessGroupCreateModalProps {
}
export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessGroupCreateModalProps) {
const { t } = useTranslation();
const [form] = Form.useForm<AccessGroupFormValues>();
const createMutation = useCreateAccessGroup();
@ -31,7 +33,7 @@ export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessG
createMutation.mutate(params, {
onSuccess: () => {
MessageManager.success("Access group created successfully");
MessageManager.success(t("accessGroups.accessGroupCreateModal.createSuccess"));
form.resetFields();
onSuccess?.();
onCancel();
@ -45,13 +47,13 @@ export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessG
return (
<Modal
title="Create Access Group"
title={t("accessGroups.accessGroupCreateModal.title")}
open={visible}
onOk={handleOk}
onCancel={onCancel}
width={700}
okText="Create Group"
cancelText="Cancel"
okText={t("accessGroups.accessGroupCreateModal.okText")}
cancelText={t("common.cancel")}
confirmLoading={createMutation.isPending}
destroyOnClose
>

View file

@ -1,5 +1,6 @@
import React, { useEffect } from "react";
import { Modal, Form } from "antd";
import { useTranslation } from "react-i18next";
import MessageManager from "@/components/molecules/message_manager";
import { AccessGroupBaseForm, AccessGroupFormValues } from "./AccessGroupBaseForm";
import { useEditAccessGroup, AccessGroupUpdateParams } from "@/app/(dashboard)/hooks/accessGroups/useEditAccessGroup";
@ -13,6 +14,7 @@ interface AccessGroupEditModalProps {
}
export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess }: AccessGroupEditModalProps) {
const { t } = useTranslation();
const [form] = Form.useForm<AccessGroupFormValues>();
const editMutation = useEditAccessGroup();
@ -45,7 +47,7 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess
{ accessGroupId: accessGroup.access_group_id, params },
{
onSuccess: () => {
MessageManager.success("Access group updated successfully");
MessageManager.success(t("accessGroups.accessGroupEditModal.updateSuccess"));
onSuccess?.();
onCancel();
},
@ -59,13 +61,13 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess
return (
<Modal
title="Edit Access Group"
title={t("accessGroups.accessGroupEditModal.title")}
open={visible}
onOk={handleOk}
onCancel={onCancel}
width={700}
okText="Save Changes"
cancelText="Cancel"
okText={t("accessGroups.accessGroupEditModal.okText")}
cancelText={t("common.cancel")}
confirmLoading={editMutation.isPending}
destroyOnHidden
>

View file

@ -13,6 +13,7 @@ import {
import { Button, Card, Flex, Input, Layout, Pagination, Space, Table, Tag, theme, Tooltip, Typography } from "antd";
import { BotIcon, LayersIcon, SearchIcon, ServerIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import {
@ -102,6 +103,7 @@ function buildAntdColumns(
}
export function AccessGroupsPage() {
const { t } = useTranslation();
const { token } = theme.useToken();
const { userRole } = useAuthorized();
// Admin Viewer follows the read-parity rule: see access groups, no writes.
@ -162,13 +164,13 @@ export function AccessGroupsPage() {
{
id: "name",
accessorKey: "name",
header: () => <span>Name</span>,
header: () => <span>{t("common.name")}</span>,
enableSorting: true,
cell: ({ getValue }) => getValue() as string,
},
{
id: "resources",
header: () => <span>Resources</span>,
header: () => <span>{t("accessGroups.accessGroupsPage.colResources")}</span>,
enableSorting: false,
cell: ({ row }) => {
const record = row.original;
@ -177,7 +179,7 @@ export function AccessGroupsPage() {
const agentIds = record.agentIds ?? [];
return (
<Flex gap={12} align="center">
<Tooltip title={`${modelIds?.length} Models`}>
<Tooltip title={t("accessGroups.accessGroupsPage.tooltipModels", { count: modelIds?.length })}>
<Tag color="blue" style={{ fontSize: 14, padding: "2px 8px", margin: 0 }}>
<Flex align="center" gap={6}>
<LayersIcon size={14} />
@ -185,7 +187,7 @@ export function AccessGroupsPage() {
</Flex>
</Tag>
</Tooltip>
<Tooltip title={`${mcpServerIds?.length} MCP Servers`}>
<Tooltip title={t("accessGroups.accessGroupsPage.tooltipMcpServers", { count: mcpServerIds?.length })}>
<Tag color="cyan" style={{ fontSize: 14, padding: "2px 8px", margin: 0 }}>
<Flex align="center" gap={6}>
<ServerIcon size={14} />
@ -193,7 +195,7 @@ export function AccessGroupsPage() {
</Flex>
</Tag>
</Tooltip>
<Tooltip title={`${agentIds?.length} Agents`}>
<Tooltip title={t("accessGroups.accessGroupsPage.tooltipAgents", { count: agentIds?.length })}>
<Tag color="purple" style={{ fontSize: 14, padding: "2px 8px", margin: 0 }}>
<Flex align="center" gap={6}>
<BotIcon size={14} />
@ -208,7 +210,7 @@ export function AccessGroupsPage() {
{
id: "createdAt",
accessorKey: "createdAt",
header: () => <span>Created</span>,
header: () => <span>{t("common.createdAt")}</span>,
enableSorting: true,
sortingFn: "datetime",
cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(),
@ -217,7 +219,7 @@ export function AccessGroupsPage() {
{
id: "updatedAt",
accessorKey: "updatedAt",
header: () => <span>Updated</span>,
header: () => <span>{t("common.updatedAt")}</span>,
enableSorting: false,
cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(),
meta: { responsive: ["xl"] },
@ -226,13 +228,13 @@ export function AccessGroupsPage() {
? [
{
id: "actions",
header: () => <span>Actions</span>,
header: () => <span>{t("common.actions")}</span>,
enableSorting: false,
cell: ({ row }: { row: Row<AccessGroup> }) => (
<Space>
<TableIconActionButton
variant="Delete"
tooltipText="Delete access group"
tooltipText={t("accessGroups.accessGroupsPage.deleteTooltip")}
onClick={() => setGroupToDelete(row.original)}
/>
</Space>
@ -243,7 +245,7 @@ export function AccessGroupsPage() {
],
// setSelectedGroup is stable (useState setter)
// eslint-disable-next-line react-hooks/exhaustive-deps
[canModify],
[canModify, t],
);
// ---------- TanStack table instance ----------
@ -281,13 +283,13 @@ export function AccessGroupsPage() {
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Space direction="vertical" size={0}>
<Title level={2} style={{ margin: 0 }}>
Access Groups
{t("accessGroups.accessGroupsPage.title")}
</Title>
<Text type="secondary">Manage resource permissions for your organization</Text>
<Text type="secondary">{t("accessGroups.accessGroupsPage.subtitle")}</Text>
</Space>
{canModify && (
<Button type="primary" icon={<PlusOutlined />} onClick={() => setIsCreateModalVisible(true)}>
Create Access Group
{t("accessGroups.accessGroupsPage.createButton")}
</Button>
)}
</Flex>
@ -302,7 +304,7 @@ export function AccessGroupsPage() {
>
<Input
prefix={<SearchIcon size={16} />}
placeholder="Search groups by name, ID, or description..."
placeholder={t("accessGroups.accessGroupsPage.searchPlaceholder")}
style={{ maxWidth: 400 }}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
@ -314,7 +316,7 @@ export function AccessGroupsPage() {
pageSize={pageSize}
onChange={(page) => setCurrentPage(page)}
size="small"
showTotal={(total) => `${total} groups`}
showTotal={(total) => t("accessGroups.accessGroupsPage.totalGroups", { total })}
showSizeChanger={false}
/>
</Flex>
@ -325,13 +327,13 @@ export function AccessGroupsPage() {
<DeleteResourceModal
isOpen={!!groupToDelete}
title="Delete Access Group"
message="Are you sure you want to delete this access group? This action cannot be undone."
resourceInformationTitle="Access Group Information"
title={t("accessGroups.accessGroupsPage.deleteTitle")}
message={t("accessGroups.accessGroupsPage.deleteMessage")}
resourceInformationTitle={t("accessGroups.accessGroupsPage.resourceInfoTitle")}
resourceInformation={[
{ label: "ID", value: groupToDelete?.id, code: true },
{ label: "Name", value: groupToDelete?.name },
{ label: "Description", value: groupToDelete?.description || "—" },
{ label: t("common.name"), value: groupToDelete?.name },
{ label: t("common.description"), value: groupToDelete?.description || "—" },
]}
onCancel={() => setGroupToDelete(null)}
onOk={() => {

View file

@ -6997,5 +6997,67 @@
"revokeConnection": "Revoke connection",
"revokeError": "Failed to revoke connection. Please try again."
}
},
"accessGroups": {
"accessGroupBaseForm": {
"tabGeneralInfo": "General Info",
"labelGroupName": "Group Name",
"nameRequired": "Please enter the access group name",
"namePlaceholder": "e.g. Engineering Team",
"descriptionPlaceholder": "Describe the purpose of this access group...",
"tabModels": "Models",
"labelAllowedModels": "Allowed Models",
"tabMcpServers": "MCP Servers",
"labelAllowedMcpServers": "Allowed MCP Servers",
"mcpServersPlaceholder": "Select MCP servers",
"tabAgents": "Agents",
"labelAllowedAgents": "Allowed Agents",
"agentsPlaceholder": "Select agents"
},
"accessGroupCreateModal": {
"title": "Create Access Group",
"okText": "Create Group",
"createSuccess": "Access group created successfully"
},
"accessGroupEditModal": {
"title": "Edit Access Group",
"okText": "Save Changes",
"updateSuccess": "Access group updated successfully"
},
"accessGroupsDetailsPage": {
"notFound": "Access group not found",
"editButton": "Edit Access Group",
"groupDetails": "Group Details",
"by": "by",
"tabModels": "Models",
"noModels": "No models assigned to this group",
"tabMcpServers": "MCP Servers",
"noMcpServers": "No MCP servers assigned to this group",
"tabAgents": "Agents",
"noAgents": "No agents assigned to this group",
"attachedKeys": "Attached Keys",
"showLess": "Show Less",
"viewAll": "View All ({{count}})",
"noKeys": "No keys attached",
"attachedTeams": "Attached Teams",
"noTeams": "No teams attached",
"created": "Created",
"lastUpdated": "Last Updated"
},
"accessGroupsPage": {
"title": "Access Groups",
"subtitle": "Manage resource permissions for your organization",
"createButton": "Create Access Group",
"searchPlaceholder": "Search groups by name, ID, or description...",
"totalGroups": "{{total}} groups",
"colResources": "Resources",
"tooltipModels": "{{count}} Models",
"tooltipMcpServers": "{{count}} MCP Servers",
"tooltipAgents": "{{count}} Agents",
"deleteTooltip": "Delete access group",
"deleteTitle": "Delete Access Group",
"deleteMessage": "Are you sure you want to delete this access group? This action cannot be undone.",
"resourceInfoTitle": "Access Group Information"
}
}
}

View file

@ -6997,5 +6997,67 @@
"revokeConnection": "撤销连接",
"revokeError": "撤销连接失败,请重试。"
}
},
"accessGroups": {
"accessGroupBaseForm": {
"tabGeneralInfo": "基本信息",
"labelGroupName": "组名称",
"nameRequired": "请输入访问组名称",
"namePlaceholder": "例如:工程团队",
"descriptionPlaceholder": "描述此访问组的用途...",
"tabModels": "模型",
"labelAllowedModels": "允许的模型",
"tabMcpServers": "MCP 服务器",
"labelAllowedMcpServers": "允许的 MCP 服务器",
"mcpServersPlaceholder": "选择 MCP 服务器",
"tabAgents": "Agent",
"labelAllowedAgents": "允许的 Agent",
"agentsPlaceholder": "选择 Agent"
},
"accessGroupCreateModal": {
"title": "创建访问组",
"okText": "创建组",
"createSuccess": "访问组创建成功"
},
"accessGroupEditModal": {
"title": "编辑访问组",
"okText": "保存更改",
"updateSuccess": "访问组更新成功"
},
"accessGroupsDetailsPage": {
"notFound": "访问组不存在",
"editButton": "编辑访问组",
"groupDetails": "组详情",
"by": "由",
"tabModels": "模型",
"noModels": "此组未分配任何模型",
"tabMcpServers": "MCP 服务器",
"noMcpServers": "此组未分配任何 MCP 服务器",
"tabAgents": "Agent",
"noAgents": "此组未分配任何 Agent",
"attachedKeys": "关联密钥",
"showLess": "收起",
"viewAll": "查看全部({{count}})",
"noKeys": "暂无关联密钥",
"attachedTeams": "关联团队",
"noTeams": "暂无关联团队",
"created": "创建时间",
"lastUpdated": "最后更新"
},
"accessGroupsPage": {
"title": "访问组",
"subtitle": "管理组织的资源访问权限",
"createButton": "创建访问组",
"searchPlaceholder": "按名称、ID 或描述搜索组...",
"totalGroups": "共 {{total}} 个组",
"colResources": "资源",
"tooltipModels": "{{count}} 个模型",
"tooltipMcpServers": "{{count}} 个 MCP 服务器",
"tooltipAgents": "{{count}} 个 Agent",
"deleteTooltip": "删除访问组",
"deleteTitle": "删除访问组",
"deleteMessage": "确定要删除此访问组吗?此操作无法撤销。",
"resourceInfoTitle": "访问组信息"
}
}
}