feat(ui): i18n for top-level components batch 8

This commit is contained in:
吹雪 2026-06-10 13:19:22 +09:00
parent eb53553d24
commit 625feb0ab7
11 changed files with 454 additions and 140 deletions

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Button } from "@tremor/react";
import { Modal } from "antd";
import { useTranslation, Trans } from "react-i18next";
import { getClaudeCodePluginsList, deleteClaudeCodePlugin } from "./networking";
import AddPluginForm from "./claude_code_plugins/add_plugin_form";
import PluginTable from "./claude_code_plugins/plugin_table";
@ -15,6 +16,7 @@ interface ClaudeCodePluginsPanelProps {
}
const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessToken, userRole }) => {
const { t } = useTranslation();
const [pluginsList, setPluginsList] = useState<Plugin[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -55,11 +57,11 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
setIsDeleting(true);
try {
await deleteClaudeCodePlugin(accessToken, pluginToDelete.name);
NotificationsManager.success(`Skill "${pluginToDelete.displayName}" deleted successfully`);
NotificationsManager.success(t("claudeCodePlugins.deleteSuccess", { name: pluginToDelete.displayName }));
fetchPlugins();
} catch (error) {
console.error("Error deleting skill:", error);
NotificationsManager.error("Failed to delete skill");
NotificationsManager.error(t("claudeCodePlugins.deleteFailed"));
} finally {
setIsDeleting(false);
setPluginToDelete(null);
@ -79,14 +81,17 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
) : (
<>
<div className="flex flex-col gap-2 mb-4">
<h1 className="text-2xl font-bold">Skills</h1>
<h1 className="text-2xl font-bold">{t("claudeCodePlugins.skillsTitle")}</h1>
<p className="text-sm text-gray-600">
Register Claude Code skills. Published skills appear in the Skill Hub for all users and are served via{" "}
<code className="bg-gray-100 px-1 rounded">/claude-code/marketplace.json</code>.
<Trans
i18nKey="claudeCodePlugins.skillsDescription"
values={{ endpoint: "/claude-code/marketplace.json" }}
components={{ code: <code className="bg-gray-100 px-1 rounded" /> }}
/>
</p>
<div className="mt-2 flex gap-2">
<Button onClick={() => setIsAddModalVisible(true)} disabled={!accessToken || !isAdmin}>
+ Add Skill
{t("claudeCodePlugins.addSkill")}
</Button>
</div>
</div>
@ -114,18 +119,22 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
{pluginToDelete && (
<Modal
title="Delete Skill"
title={t("claudeCodePlugins.deleteSkillTitle")}
open={pluginToDelete !== null}
onOk={handleDeleteConfirm}
onCancel={() => setPluginToDelete(null)}
confirmLoading={isDeleting}
okText="Delete"
okText={t("common.delete")}
okButtonProps={{ danger: true }}
>
<p>
Are you sure you want to delete skill: <strong>{pluginToDelete.displayName}</strong>?
<Trans
i18nKey="claudeCodePlugins.deleteConfirmMessage"
values={{ name: pluginToDelete.displayName }}
components={{ strong: <strong /> }}
/>
</p>
<p>This action cannot be undone.</p>
<p>{t("claudeCodePlugins.cannotUndone")}</p>
</Modal>
)}
</div>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Text, Button, Callout, TextInput } from "@tremor/react";
import { Modal, Form, Spin, Select } from "antd";
import { useTranslation } from "react-i18next";
import { getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
@ -24,6 +25,7 @@ interface CloudZeroSettingsView {
type ExportType = "cloudzero" | "csv";
const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onClose, accessToken }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [existingSettings, setExistingSettings] = useState<CloudZeroSettingsView | null>(null);
@ -59,11 +61,13 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
} else if (response.status !== 404) {
// 404 means no settings configured yet, which is fine
const errorData = await response.json();
NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || "Unknown error"}`);
NotificationsManager.fromBackend(
t("cloudzeroExportModal.failedToLoadSettings", { error: errorData.error || t("common.unknown") }),
);
}
} catch (error) {
console.error("Error loading CloudZero settings:", error);
NotificationsManager.fromBackend("Failed to load existing settings");
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToLoadSettingsGeneric"));
} finally {
setSettingsLoading(false);
}
@ -71,7 +75,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const handleSaveCloudZeroSettings = async (values: CloudZeroSettings) => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("cloudzeroExportModal.noAccessToken"));
return;
}
@ -98,7 +102,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const data = await response.json();
if (response.ok) {
NotificationsManager.success(data.message || "CloudZero settings saved successfully");
NotificationsManager.success(data.message || t("cloudzeroExportModal.settingsSavedSuccess"));
setExistingSettings({
api_key_masked: values.api_key.substring(0, 4) + "****" + values.api_key.slice(-4),
connection_id: values.connection_id,
@ -106,12 +110,12 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
});
return true;
} else {
NotificationsManager.fromBackend(data.error || "Failed to save CloudZero settings");
NotificationsManager.fromBackend(data.error || t("cloudzeroExportModal.failedToSaveSettings"));
return false;
}
} catch (error) {
console.error("Error saving CloudZero settings:", error);
NotificationsManager.fromBackend("Failed to save CloudZero settings");
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToSaveSettings"));
return false;
} finally {
setLoading(false);
@ -120,7 +124,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const handleExportCloudZero = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("cloudzeroExportModal.noAccessToken"));
return;
}
@ -141,14 +145,14 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const data = await response.json();
if (response.ok) {
NotificationsManager.success(data.message || "Export to CloudZero completed successfully");
NotificationsManager.success(data.message || t("cloudzeroExportModal.exportSuccess"));
onClose();
} else {
NotificationsManager.fromBackend(data.error || "Failed to export to CloudZero");
NotificationsManager.fromBackend(data.error || t("cloudzeroExportModal.failedToExport"));
}
} catch (error) {
console.error("Error exporting to CloudZero:", error);
NotificationsManager.fromBackend("Failed to export to CloudZero");
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToExport"));
} finally {
setExportLoading(false);
}
@ -158,11 +162,11 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
setExportLoading(true);
try {
// TODO: Implement CSV export functionality
NotificationsManager.info("CSV export functionality coming soon!");
NotificationsManager.info(t("cloudzeroExportModal.csvComingSoon"));
onClose();
} catch (error) {
console.error("Error exporting CSV:", error);
NotificationsManager.fromBackend("Failed to export CSV");
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToExportCsv"));
} finally {
setExportLoading(false);
}
@ -203,7 +207,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<span>Export to CloudZero</span>
<span>{t("cloudzeroExportModal.exportToCloudZero")}</span>
</div>
),
},
@ -219,18 +223,25 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span>Export to CSV</span>
<span>{t("cloudzeroExportModal.exportToCsv")}</span>
</div>
),
},
];
return (
<Modal title="Export Data" open={isOpen} onCancel={handleModalClose} footer={null} width={600} destroyOnHidden>
<Modal
title={t("cloudzeroExportModal.exportDataTitle")}
open={isOpen}
onCancel={handleModalClose}
footer={null}
width={600}
destroyOnHidden
>
<div className="space-y-4">
{/* Export Type Selection */}
<div>
<Text className="font-medium mb-2 block">Export Destination</Text>
<Text className="font-medium mb-2 block">{t("cloudzeroExportModal.exportDestination")}</Text>
<Select value={exportType} onChange={setExportType} options={exportOptions} className="w-full" size="large" />
</div>
@ -245,7 +256,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
<>
{existingSettings && (
<Callout
title="Existing CloudZero Configuration"
title={t("cloudzeroExportModal.existingConfig")}
icon={() => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -260,9 +271,9 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
className="mb-4"
>
<Text>
API Key: {existingSettings.api_key_masked}
{t("cloudzeroExportModal.apiKeyInfo", { maskedKey: existingSettings.api_key_masked })}
<br />
Connection ID: {existingSettings.connection_id}
{t("cloudzeroExportModal.connectionIdInfo", { connectionId: existingSettings.connection_id })}
</Text>
</Callout>
)}
@ -270,19 +281,19 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
{!existingSettings && (
<Form form={form} layout="vertical">
<Form.Item
label="CloudZero API Key"
label={t("cloudzeroExportModal.apiKeyLabel")}
name="api_key"
rules={[{ required: true, message: "Please enter your CloudZero API key" }]}
rules={[{ required: true, message: t("cloudzeroExportModal.apiKeyRequired") }]}
>
<TextInput type="password" placeholder="Enter your CloudZero API key" />
<TextInput type="password" placeholder={t("cloudzeroExportModal.apiKeyPlaceholder")} />
</Form.Item>
<Form.Item
label="Connection ID"
label={t("cloudzeroExportModal.connectionIdLabel")}
name="connection_id"
rules={[{ required: true, message: "Please enter the CloudZero connection ID" }]}
rules={[{ required: true, message: t("cloudzeroExportModal.connectionIdRequired") }]}
>
<TextInput placeholder="Enter CloudZero connection ID" />
<TextInput placeholder={t("cloudzeroExportModal.connectionIdPlaceholder")} />
</Form.Item>
</Form>
)}
@ -294,7 +305,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
{/* CSV Export Info */}
{exportType === "csv" && (
<Callout
title="CSV Export"
title={t("cloudzeroExportModal.csvExportTitle")}
icon={() => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -302,17 +313,19 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
)}
color="blue"
>
<Text>Export your usage data as a CSV file for analysis in spreadsheet applications.</Text>
<Text>{t("cloudzeroExportModal.csvExportDesc")}</Text>
</Callout>
)}
{/* Action Buttons */}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="secondary" onClick={handleModalClose}>
Cancel
{t("common.cancel")}
</Button>
<Button onClick={handleExport} loading={loading || exportLoading} disabled={loading || exportLoading}>
{exportType === "cloudzero" ? "Export to CloudZero" : "Export CSV"}
{exportType === "cloudzero"
? t("cloudzeroExportModal.exportToCloudZero")
: t("cloudzeroExportModal.exportCsvButton")}
</Button>
</div>
</div>

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { TextInput, SelectItem } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { Button as Button2, Modal, Form, Select as Select2, InputNumber } from "antd";
@ -15,6 +16,7 @@ interface EditUserModalProps {
}
const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => {
const { t } = useTranslation();
const [editedUser, setEditedUser] = useState(user);
const [form] = Form.useForm();
@ -39,7 +41,13 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
}
return (
<Modal open={visible} onCancel={handleCancel} footer={null} title={"Edit User " + user.user_id} width={1000}>
<Modal
open={visible}
onCancel={handleCancel}
footer={null}
title={t("editUser.modalTitle", { userId: user.user_id })}
width={1000}
>
<Form
form={form}
onFinish={handleEditSubmit}
@ -49,7 +57,12 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
labelAlign="left"
>
<>
<Form.Item className="mt-8" label="User Email" tooltip="Email of the User" name="user_email">
<Form.Item
className="mt-8"
label={t("editUser.userEmailLabel")}
tooltip={t("editUser.userEmailTooltip")}
name="user_email"
>
<TextInput />
</Form.Item>
@ -57,7 +70,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
<TextInput />
</Form.Item>
<Form.Item label="User Role" name="user_role">
<Form.Item label={t("editUser.userRoleLabel")} name="user_role">
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
@ -74,33 +87,33 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
</Form.Item>
<Form.Item
label="Spend (USD)"
label={t("editUser.spendLabel")}
name="spend"
tooltip="(float) - Spend of all LLM calls completed by this user"
help="Across all keys (including keys with team_id)."
tooltip={t("editUser.spendTooltip")}
help={t("editUser.spendHelp")}
>
<InputNumber min={0} step={0.01} />
</Form.Item>
<Form.Item
label="User Budget (USD)"
label={t("editUser.maxBudgetLabel")}
name="max_budget"
tooltip="(float) - Maximum budget of this user"
help="Maximum budget of this user."
tooltip={t("editUser.maxBudgetTooltip")}
help={t("editUser.maxBudgetHelp")}
>
<NumericalInput min={0} step={0.01} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Form.Item label={t("editUser.resetBudgetLabel")} name="budget_duration">
<BudgetDurationDropdown />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Save</Button2>
<Button2 htmlType="submit">{t("common.save")}</Button2>
</div>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Save</Button2>
<Button2 htmlType="submit">{t("common.save")}</Button2>
</div>
</>
</Form>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Card, Text, Grid, Button, TextInput, TableCell } from "@tremor/react";
import { Typography } from "antd";
import { useTranslation } from "react-i18next";
import NotificationManager from "./molecules/notifications_manager";
import { serviceHealthCheck, setCallbacksCall } from "./networking";
import { EmailEventSettings } from "./email_events";
@ -14,6 +15,7 @@ interface EmailSettingsProps {
}
const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser, alerts }) => {
const { t } = useTranslation();
const handleSaveEmailSettings = async () => {
if (!accessToken) {
return;
@ -43,7 +45,7 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
};
try {
await setCallbacksCall(accessToken, payload);
NotificationManager.success("Email settings updated successfully");
NotificationManager.success(t("emailSettings.saveSuccess"));
} catch (error) {
NotificationManager.fromBackend(error);
}
@ -55,11 +57,11 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
<EmailEventSettings accessToken={accessToken} />
</div>
<Card>
<Title level={4}>Email Server Settings</Title>
<Title level={4}>{t("emailSettings.emailServerSettings")}</Title>
<Text>
<a href="https://docs.litellm.ai/docs/proxy/email" target="_blank" style={{ color: "blue" }}>
{" "}
LiteLLM Docs: email alerts
{t("emailSettings.docsLink")}
</a>{" "}
<br />
</Text>
@ -102,50 +104,47 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
<p style={{ fontSize: "small", fontStyle: "italic" }}>
{key === "SMTP_HOST" && (
<div style={{ color: "gray" }}>
Enter the SMTP host address, e.g. `smtp.resend.com`
<span style={{ color: "red" }}> Required * </span>
{t("emailSettings.smtpHostDesc")}
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
</div>
)}
{key === "SMTP_PORT" && (
<div style={{ color: "gray" }}>
Enter the SMTP port number, e.g. `587`
<span style={{ color: "red" }}> Required * </span>
{t("emailSettings.smtpPortDesc")}
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
</div>
)}
{key === "SMTP_USERNAME" && (
<div style={{ color: "gray" }}>
Enter the SMTP username, e.g. `username`
<span style={{ color: "red" }}> Required * </span>
{t("emailSettings.smtpUsernameDesc")}
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
</div>
)}
{key === "SMTP_PASSWORD" && <span style={{ color: "red" }}> Required * </span>}
{key === "SMTP_PASSWORD" && (
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
)}
{key === "SMTP_SENDER_EMAIL" && (
<div style={{ color: "gray" }}>
Enter the sender email address, e.g. `sender@berri.ai`
<span style={{ color: "red" }}> Required * </span>
{t("emailSettings.smtpSenderEmailDesc")}
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
</div>
)}
{key === "TEST_EMAIL_ADDRESS" && (
<div style={{ color: "gray" }}>
Email Address to send `Test Email Alert` to. example: `info@berri.ai`
<span style={{ color: "red" }}> Required * </span>
{t("emailSettings.testEmailAddressDesc")}
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
</div>
)}
{key === "EMAIL_LOGO_URL" && (
<div style={{ color: "gray" }}>
(Optional) Customize the Logo that appears in the email, pass a url to your logo
</div>
<div style={{ color: "gray" }}>{t("emailSettings.emailLogoUrlDesc")}</div>
)}
{key === "EMAIL_SUPPORT_CONTACT" && (
<div style={{ color: "gray" }}>
(Optional) Customize the support email address that appears in the email. Default is
support@berri.ai
</div>
<div style={{ color: "gray" }}>{t("emailSettings.emailSupportContactDesc")}</div>
)}
</p>
</li>
@ -157,21 +156,21 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
</div>
<Button className="mt-2" onClick={() => handleSaveEmailSettings()}>
Save Changes
{t("emailSettings.saveChanges")}
</Button>
<Button
onClick={async () => {
if (!accessToken) return;
try {
await serviceHealthCheck(accessToken, "email");
NotificationManager.success("Email test triggered. Check your configured email inbox/logs.");
NotificationManager.success(t("emailSettings.testSuccess"));
} catch (error) {
NotificationManager.fromBackend(error);
}
}}
className="mx-2"
>
Test Email Alerts
{t("emailSettings.testEmailAlerts")}
</Button>
</Card>
</>

View file

@ -14,6 +14,7 @@ import {
Switch,
} from "@tremor/react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "./networking";
import { InputNumber } from "antd";
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
@ -37,6 +38,7 @@ interface generalSettingsItem {
}
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID, modelData }) => {
const { t } = useTranslation();
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
useEffect(() => {
@ -106,10 +108,10 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
<div className="w-full">
<TabGroup className="h-[75vh] w-full">
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Routing Groups</Tab>
<Tab value="3">Fallbacks</Tab>
<Tab value="4">General</Tab>
<Tab value="1">{t("generalSettings.tabLoadbalancing")}</Tab>
<Tab value="2">{t("generalSettings.tabRoutingGroups")}</Tab>
<Tab value="3">{t("generalSettings.tabFallbacks")}</Tab>
<Tab value="4">{t("generalSettings.tabGeneral")}</Tab>
</TabList>
<TabPanels className="px-8 py-6">
<TabPanel>
@ -126,10 +128,10 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
<TableHeaderCell>{t("generalSettings.colSetting")}</TableHeaderCell>
<TableHeaderCell>{t("generalSettings.colValue")}</TableHeaderCell>
<TableHeaderCell>{t("generalSettings.colStatus")}</TableHeaderCell>
<TableHeaderCell>{t("generalSettings.colAction")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -167,16 +169,18 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
<TableCell>
{value.stored_in_db == true ? (
<Badge icon={CheckCircleIcon} className="text-white">
In DB
{t("generalSettings.statusInDb")}
</Badge>
) : value.stored_in_db == false ? (
<Badge className="text-gray bg-white outline">In Config</Badge>
<Badge className="text-gray bg-white outline">{t("generalSettings.statusInConfig")}</Badge>
) : (
<Badge className="text-gray bg-white outline">Not Set</Badge>
<Badge className="text-gray bg-white outline">{t("generalSettings.statusNotSet")}</Badge>
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name, index)}>Update</Button>
<Button onClick={() => handleUpdateField(value.field_name, index)}>
{t("common.update")}
</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
Reset
</Icon>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Button, Dropdown, Tabs } from "antd";
import { DownOutlined, PlusOutlined, CodeOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
import AddGuardrailForm from "./guardrails/add_guardrail_form";
import GuardrailTable from "./guardrails/guardrail_table";
@ -39,6 +40,7 @@ interface GuardrailsResponse {
}
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
const { t } = useTranslation();
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
@ -108,11 +110,11 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
NotificationsManager.success(t("guardrails.deleteSuccess", { name: guardrailToDelete.guardrail_name }));
await fetchGuardrails();
} catch (error) {
console.error("Error deleting guardrail:", error);
NotificationsManager.fromBackend("Failed to delete guardrail");
NotificationsManager.fromBackend(t("guardrails.deleteFailed"));
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
@ -139,12 +141,12 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
? [
{
key: "garden",
label: "Guardrail Garden",
label: t("guardrails.guardrailGardenTab"),
children: <GuardrailGarden accessToken={accessToken} onGuardrailCreated={handleSuccess} />,
},
{
key: "guardrails",
label: "Guardrails",
label: t("guardrails.guardrailsTab"),
children: (
<>
<div className="flex justify-between items-center mb-4">
@ -154,13 +156,13 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
{
key: "provider",
icon: <PlusOutlined />,
label: "Add Provider Guardrail",
label: t("guardrails.addProviderGuardrail"),
onClick: handleAddGuardrail,
},
{
key: "custom_code",
icon: <CodeOutlined />,
label: "Create Custom Code Guardrail",
label: t("guardrails.createCustomCodeGuardrail"),
onClick: handleAddCustomCodeGuardrail,
},
],
@ -169,7 +171,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
disabled={!accessToken}
>
<Button disabled={!accessToken}>
+ Add New Guardrail <DownOutlined className="ml-2" />
{t("guardrails.addNewGuardrail")} <DownOutlined className="ml-2" />
</Button>
</Dropdown>
</div>
@ -209,17 +211,17 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Guardrail"
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
resourceInformationTitle="Guardrail Information"
title={t("guardrails.deleteGuardrailTitle")}
message={t("guardrails.deleteGuardrailMessage", { name: guardrailToDelete?.guardrail_name })}
resourceInformationTitle={t("guardrails.guardrailInfoTitle")}
resourceInformation={[
{ label: "Name", value: guardrailToDelete?.guardrail_name },
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
{ label: t("common.name"), value: guardrailToDelete?.guardrail_name },
{ label: t("guardrails.labelId"), value: guardrailToDelete?.guardrail_id, code: true },
{ label: t("common.type"), value: providerDisplayName },
{ label: t("guardrails.labelMode"), value: guardrailToDelete?.litellm_params.mode },
{
label: "Default On",
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
label: t("guardrails.labelDefaultOn"),
value: guardrailToDelete?.litellm_params.default_on ? t("common.yes") : t("common.no"),
},
]}
onCancel={handleDeleteCancel}
@ -231,7 +233,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
},
{
key: "playground",
label: "Test Playground",
label: t("guardrails.testPlaygroundTab"),
disabled: !accessToken,
children: (
<GuardrailTestPlayground
@ -246,7 +248,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
: []),
{
key: "submitted",
label: "Submitted Guardrails",
label: t("guardrails.submittedGuardrailsTab"),
children: <TeamGuardrailsTab accessToken={accessToken} />,
},
]}

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Button, Space } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
import { useTranslation } from "react-i18next";
interface KeyValueInputProps {
value?: Record<string, string>;
@ -9,6 +10,7 @@ interface KeyValueInputProps {
}
const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) => {
const { t } = useTranslation();
const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value));
const handleAdd = () => {
@ -32,9 +34,13 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
<div>
{pairs.map(([key, val], index) => (
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="center">
<TextInput placeholder="Header Name" value={key} onChange={(e) => handleChange(index, e.target.value, val)} />
<TextInput
placeholder="Header Value"
placeholder={t("keyValueInput.headerNamePlaceholder")}
value={key}
onChange={(e) => handleChange(index, e.target.value, val)}
/>
<TextInput
placeholder={t("keyValueInput.headerValuePlaceholder")}
value={val}
onChange={(e) => handleChange(index, key, e.target.value)}
/>
@ -44,7 +50,7 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
</Space>
))}
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
Add Header
{t("keyValueInput.addHeader")}
</Button>
</div>
);

View file

@ -1,6 +1,7 @@
import React from "react";
import { Tag } from "antd";
import { CogIcon, BanIcon } from "@heroicons/react/outline";
import { useTranslation } from "react-i18next";
import { callbackInfo, callback_map, reverse_callback_map } from "./callback_info_helpers";
interface LoggingConfig {
@ -22,6 +23,7 @@ export function LoggingSettingsView({
variant = "card",
className = "",
}: LoggingSettingsViewProps) {
const { t } = useTranslation();
const getLoggingDisplayName = (callbackName: string) => {
// Find the display name for the callback
const callbackDisplayName = Object.entries(callback_map).find(([_, value]) => value === callbackName)?.[0];
@ -44,11 +46,11 @@ export function LoggingSettingsView({
const getEventTypeLabel = (eventType: string) => {
switch (eventType) {
case "success":
return "Success Only";
return t("loggingSettingsView.successOnly");
case "failure":
return "Failure Only";
return t("loggingSettingsView.failureOnly");
case "success_and_failure":
return "Success & Failure";
return t("loggingSettingsView.successAndFailure");
default:
return eventType;
}
@ -60,7 +62,7 @@ export function LoggingSettingsView({
<div className="space-y-3">
<div className="flex items-center gap-2">
<CogIcon className="h-4 w-4 text-blue-600" />
<span className="font-semibold text-gray-900">Logging Integrations</span>
<span className="font-semibold text-gray-900">{t("loggingSettingsView.loggingIntegrations")}</span>
<Tag color="blue">{loggingConfigs.length}</Tag>
</div>
@ -84,7 +86,9 @@ export function LoggingSettingsView({
<div>
<span className="block font-medium text-blue-800">{displayName}</span>
<span className="block text-xs text-blue-600">
{Object.keys(config.callback_vars).length} parameters configured
{t("loggingSettingsView.parametersConfigured", {
count: Object.keys(config.callback_vars).length,
})}
</span>
</div>
</div>
@ -96,7 +100,7 @@ export function LoggingSettingsView({
) : (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
<CogIcon className="h-4 w-4 text-gray-400" />
<span className="text-gray-500 text-sm">No logging integrations configured</span>
<span className="text-gray-500 text-sm">{t("loggingSettingsView.noIntegrationsConfigured")}</span>
</div>
)}
</div>
@ -105,7 +109,7 @@ export function LoggingSettingsView({
<div className="space-y-3">
<div className="flex items-center gap-2">
<BanIcon className="h-4 w-4 text-red-600" />
<span className="font-semibold text-gray-900">Disabled Callbacks</span>
<span className="font-semibold text-gray-900">{t("loggingSettingsView.disabledCallbacks")}</span>
<Tag color="red">{disabledCallbacks.length}</Tag>
</div>
@ -129,10 +133,10 @@ export function LoggingSettingsView({
)}
<div>
<span className="block font-medium text-red-800">{displayName}</span>
<span className="block text-xs text-red-600">Disabled for this key</span>
<span className="block text-xs text-red-600">{t("loggingSettingsView.disabledForKey")}</span>
</div>
</div>
<Tag color="red">Disabled</Tag>
<Tag color="red">{t("common.disabled")}</Tag>
</div>
);
})}
@ -140,7 +144,7 @@ export function LoggingSettingsView({
) : (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
<BanIcon className="h-4 w-4 text-gray-400" />
<span className="text-gray-500 text-sm">No callbacks disabled</span>
<span className="text-gray-500 text-sm">{t("loggingSettingsView.noCallbacksDisabled")}</span>
</div>
)}
</div>
@ -152,10 +156,8 @@ export function LoggingSettingsView({
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<span className="block font-semibold text-gray-900">Logging Settings</span>
<span className="block text-xs text-gray-500">
Active logging integrations and disabled callbacks for this key
</span>
<span className="block font-semibold text-gray-900">{t("loggingSettingsView.cardTitle")}</span>
<span className="block text-xs text-gray-500">{t("loggingSettingsView.cardSubtitle")}</span>
</div>
</div>
{content}
@ -165,7 +167,7 @@ export function LoggingSettingsView({
return (
<div className={`${className}`}>
<span className="block font-medium text-gray-900 mb-3">Logging Settings</span>
<span className="block font-medium text-gray-900 mb-3">{t("loggingSettingsView.inlineTitle")}</span>
{content}
</div>
);

View file

@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import i18n from "@/lib/i18n";
export interface MCPServerData {
server_id: string;
@ -36,9 +37,10 @@ export const mcpHubColumns = (
copyToClipboard: (text: string) => void,
publicPage: boolean = false,
): ColumnDef<MCPServerData>[] => {
const t = i18n.t.bind(i18n);
const allColumns: ColumnDef<MCPServerData>[] = [
{
header: "Server Name",
header: t("mcpHubTableColumns.colServerName"),
accessorKey: "server_name",
enableSorting: true,
sortingFn: "alphanumeric",
@ -49,7 +51,7 @@ export const mcpHubColumns = (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{server.server_name}</Text>
<Tooltip title="Copy server name">
<Tooltip title={t("mcpHubTableColumns.copyServerName")}>
<CopyOutlined
onClick={() => copyToClipboard(server.server_name)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
@ -65,7 +67,7 @@ export const mcpHubColumns = (
},
},
{
header: "Description",
header: t("common.description"),
accessorKey: "description",
enableSorting: true,
sortingFn: "alphanumeric",
@ -79,7 +81,7 @@ export const mcpHubColumns = (
},
},
{
header: "URL",
header: t("mcpHubTableColumns.colUrl"),
accessorKey: "url",
enableSorting: true,
sortingFn: "alphanumeric",
@ -89,7 +91,7 @@ export const mcpHubColumns = (
return (
<div className="flex items-center space-x-2">
<Text className="text-xs truncate max-w-xs">{server.url}</Text>
<Tooltip title="Copy URL">
<Tooltip title={t("mcpHubTableColumns.copyUrl")}>
<CopyOutlined
onClick={() => copyToClipboard(server.url)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"
@ -103,7 +105,7 @@ export const mcpHubColumns = (
},
},
{
header: "Transport",
header: t("mcpHubTableColumns.colTransport"),
accessorKey: "transport",
enableSorting: true,
sortingFn: "alphanumeric",
@ -121,7 +123,7 @@ export const mcpHubColumns = (
},
},
{
header: "Auth Type",
header: t("mcpHubTableColumns.colAuthType"),
accessorKey: "auth_type",
enableSorting: true,
sortingFn: "alphanumeric",
@ -141,7 +143,7 @@ export const mcpHubColumns = (
},
},
{
header: "Status",
header: t("common.status"),
accessorKey: "status",
enableSorting: true,
sortingFn: "alphanumeric",
@ -166,7 +168,7 @@ export const mcpHubColumns = (
},
},
{
header: "Tools",
header: t("mcpHubTableColumns.colTools"),
accessorKey: "allowed_tools",
enableSorting: false,
cell: ({ row }) => {
@ -176,7 +178,9 @@ export const mcpHubColumns = (
return (
<div className="space-y-1">
<Text className="text-xs font-medium">
{tools.length > 0 ? `${tools.length} tool${tools.length !== 1 ? "s" : ""}` : "All tools"}
{tools.length > 0
? t("mcpHubTableColumns.toolCount", { count: tools.length })
: t("mcpHubTableColumns.allTools")}
</Text>
{tools.length > 0 && (
<div className="flex flex-wrap gap-1">
@ -196,7 +200,7 @@ export const mcpHubColumns = (
},
},
{
header: "Created By",
header: t("mcpHubTableColumns.colCreatedBy"),
accessorKey: "created_by",
enableSorting: true,
sortingFn: "alphanumeric",
@ -210,7 +214,7 @@ export const mcpHubColumns = (
},
},
{
header: "Public",
header: t("mcpHubTableColumns.colPublic"),
accessorKey: "mcp_info.is_public",
enableSorting: true,
sortingFn: (rowA, rowB) => {
@ -223,11 +227,11 @@ export const mcpHubColumns = (
return server.mcp_info?.is_public === true ? (
<Badge color="green" size="xs">
Yes
{t("common.yes")}
</Badge>
) : (
<Badge color="gray" size="xs">
No
{t("common.no")}
</Badge>
);
},
@ -236,7 +240,7 @@ export const mcpHubColumns = (
},
},
{
header: "Details",
header: t("common.details"),
id: "details",
enableSorting: false,
cell: ({ row }) => {
@ -244,8 +248,8 @@ export const mcpHubColumns = (
return (
<Button size="xs" variant="secondary" onClick={() => showModal(server)} icon={InfoCircleOutlined}>
<span className="hidden lg:inline">Details</span>
<span className="lg:hidden">Info</span>
<span className="hidden lg:inline">{t("common.details")}</span>
<span className="lg:hidden">{t("mcpHubTableColumns.infoShort")}</span>
</Button>
);
},

View file

@ -1128,6 +1128,137 @@
"errorParsingDetails": "Error parsing details",
"jsonFormatError": "Error formatting JSON: {{error}}"
},
"claudeCodePlugins": {
"skillsTitle": "Skills",
"skillsDescription": "Register Claude Code skills. Published skills appear in the Skill Hub for all users and are served via {{endpoint}}.",
"addSkill": "+ Add Skill",
"deleteSkillTitle": "Delete Skill",
"deleteConfirmMessage": "Are you sure you want to delete skill: <strong>{{name}}</strong>?",
"cannotUndone": "This action cannot be undone.",
"deleteSuccess": "Skill \"{{name}}\" deleted successfully",
"deleteFailed": "Failed to delete skill"
},
"cloudzeroExportModal": {
"exportDataTitle": "Export Data",
"exportDestination": "Export Destination",
"exportToCloudZero": "Export to CloudZero",
"exportToCsv": "Export to CSV",
"existingConfig": "Existing CloudZero Configuration",
"apiKeyInfo": "API Key: {{maskedKey}}",
"connectionIdInfo": "Connection ID: {{connectionId}}",
"apiKeyLabel": "CloudZero API Key",
"apiKeyRequired": "Please enter your CloudZero API key",
"apiKeyPlaceholder": "Enter your CloudZero API key",
"connectionIdLabel": "Connection ID",
"connectionIdRequired": "Please enter the CloudZero connection ID",
"connectionIdPlaceholder": "Enter CloudZero connection ID",
"csvExportTitle": "CSV Export",
"csvExportDesc": "Export your usage data as a CSV file for analysis in spreadsheet applications.",
"exportCsvButton": "Export CSV",
"settingsSavedSuccess": "CloudZero settings saved successfully",
"exportSuccess": "Export to CloudZero completed successfully",
"csvComingSoon": "CSV export functionality coming soon!",
"noAccessToken": "No access token available",
"failedToLoadSettings": "Failed to load existing settings: {{error}}",
"failedToLoadSettingsGeneric": "Failed to load existing settings",
"failedToSaveSettings": "Failed to save CloudZero settings",
"failedToExport": "Failed to export to CloudZero",
"failedToExportCsv": "Failed to export CSV"
},
"editUser": {
"modalTitle": "Edit User {{userId}}",
"userEmailLabel": "User Email",
"userEmailTooltip": "Email of the User",
"userRoleLabel": "User Role",
"spendLabel": "Spend (USD)",
"spendTooltip": "(float) - Spend of all LLM calls completed by this user",
"spendHelp": "Across all keys (including keys with team_id).",
"maxBudgetLabel": "User Budget (USD)",
"maxBudgetTooltip": "(float) - Maximum budget of this user",
"maxBudgetHelp": "Maximum budget of this user.",
"resetBudgetLabel": "Reset Budget"
},
"emailSettings": {
"emailServerSettings": "Email Server Settings",
"docsLink": "LiteLLM Docs: email alerts",
"saveChanges": "Save Changes",
"testEmailAlerts": "Test Email Alerts",
"saveSuccess": "Email settings updated successfully",
"testSuccess": "Email test triggered. Check your configured email inbox/logs.",
"requiredStar": "Required *",
"smtpHostDesc": "Enter the SMTP host address, e.g. `smtp.resend.com`",
"smtpPortDesc": "Enter the SMTP port number, e.g. `587`",
"smtpUsernameDesc": "Enter the SMTP username, e.g. `username`",
"smtpSenderEmailDesc": "Enter the sender email address, e.g. `sender@berri.ai`",
"testEmailAddressDesc": "Email Address to send `Test Email Alert` to. example: `info@berri.ai`",
"emailLogoUrlDesc": "(Optional) Customize the Logo that appears in the email, pass a url to your logo",
"emailSupportContactDesc": "(Optional) Customize the support email address that appears in the email. Default is support@berri.ai"
},
"generalSettings": {
"tabLoadbalancing": "Loadbalancing",
"tabRoutingGroups": "Routing Groups",
"tabFallbacks": "Fallbacks",
"tabGeneral": "General",
"colSetting": "Setting",
"colValue": "Value",
"colStatus": "Status",
"colAction": "Action",
"statusInDb": "In DB",
"statusInConfig": "In Config",
"statusNotSet": "Not Set"
},
"guardrails": {
"guardrailGardenTab": "Guardrail Garden",
"guardrailsTab": "Guardrails",
"testPlaygroundTab": "Test Playground",
"submittedGuardrailsTab": "Submitted Guardrails",
"addNewGuardrail": "+ Add New Guardrail",
"addProviderGuardrail": "Add Provider Guardrail",
"createCustomCodeGuardrail": "Create Custom Code Guardrail",
"deleteGuardrailTitle": "Delete Guardrail",
"deleteGuardrailMessage": "Are you sure you want to delete guardrail: {{name}}? This action cannot be undone.",
"guardrailInfoTitle": "Guardrail Information",
"labelId": "ID",
"labelMode": "Mode",
"labelDefaultOn": "Default On",
"deleteSuccess": "Guardrail \"{{name}}\" deleted successfully",
"deleteFailed": "Failed to delete guardrail"
},
"keyValueInput": {
"headerNamePlaceholder": "Header Name",
"headerValuePlaceholder": "Header Value",
"addHeader": "Add Header"
},
"loggingSettingsView": {
"loggingIntegrations": "Logging Integrations",
"parametersConfigured_one": "{{count}} parameter configured",
"parametersConfigured_other": "{{count}} parameters configured",
"noIntegrationsConfigured": "No logging integrations configured",
"disabledCallbacks": "Disabled Callbacks",
"disabledForKey": "Disabled for this key",
"noCallbacksDisabled": "No callbacks disabled",
"cardTitle": "Logging Settings",
"cardSubtitle": "Active logging integrations and disabled callbacks for this key",
"inlineTitle": "Logging Settings",
"successOnly": "Success Only",
"failureOnly": "Failure Only",
"successAndFailure": "Success & Failure"
},
"mcpHubTableColumns": {
"colServerName": "Server Name",
"colUrl": "URL",
"colTransport": "Transport",
"colAuthType": "Auth Type",
"colTools": "Tools",
"colCreatedBy": "Created By",
"colPublic": "Public",
"copyServerName": "Copy server name",
"copyUrl": "Copy URL",
"toolCount_one": "{{count}} tool",
"toolCount_other": "{{count}} tools",
"allTools": "All tools",
"infoShort": "Info"
},
"common": {
"save": "Save",
"cancel": "Cancel",

View file

@ -1128,6 +1128,137 @@
"errorParsingDetails": "解析详情时出错",
"jsonFormatError": "JSON 格式化出错:{{error}}"
},
"claudeCodePlugins": {
"skillsTitle": "技能",
"skillsDescription": "注册 Claude Code 技能。已发布的技能将在技能中心向所有用户展示,并通过 {{endpoint}} 提供服务。",
"addSkill": "+ 添加技能",
"deleteSkillTitle": "删除技能",
"deleteConfirmMessage": "确定要删除技能:<strong>{{name}}</strong>?",
"cannotUndone": "此操作无法撤销。",
"deleteSuccess": "技能「{{name}}」已成功删除",
"deleteFailed": "删除技能失败"
},
"cloudzeroExportModal": {
"exportDataTitle": "导出数据",
"exportDestination": "导出目标",
"exportToCloudZero": "导出到 CloudZero",
"exportToCsv": "导出为 CSV",
"existingConfig": "现有 CloudZero 配置",
"apiKeyInfo": "API 密钥:{{maskedKey}}",
"connectionIdInfo": "连接 ID:{{connectionId}}",
"apiKeyLabel": "CloudZero API 密钥",
"apiKeyRequired": "请输入您的 CloudZero API 密钥",
"apiKeyPlaceholder": "输入您的 CloudZero API 密钥",
"connectionIdLabel": "连接 ID",
"connectionIdRequired": "请输入 CloudZero 连接 ID",
"connectionIdPlaceholder": "输入 CloudZero 连接 ID",
"csvExportTitle": "CSV 导出",
"csvExportDesc": "将您的用量数据导出为 CSV 文件,以便在电子表格应用中分析。",
"exportCsvButton": "导出 CSV",
"settingsSavedSuccess": "CloudZero 设置已保存",
"exportSuccess": "已成功导出到 CloudZero",
"csvComingSoon": "CSV 导出功能即将推出!",
"noAccessToken": "无可用访问令牌",
"failedToLoadSettings": "加载现有设置失败:{{error}}",
"failedToLoadSettingsGeneric": "加载现有设置失败",
"failedToSaveSettings": "保存 CloudZero 设置失败",
"failedToExport": "导出到 CloudZero 失败",
"failedToExportCsv": "导出 CSV 失败"
},
"editUser": {
"modalTitle": "编辑用户 {{userId}}",
"userEmailLabel": "用户邮箱",
"userEmailTooltip": "用户的邮箱地址",
"userRoleLabel": "用户角色",
"spendLabel": "花费(USD)",
"spendTooltip": "(浮点数)- 该用户完成的所有 LLM 调用的花费",
"spendHelp": "包含所有密钥(含有 team_id 的密钥)的花费。",
"maxBudgetLabel": "用户预算(USD)",
"maxBudgetTooltip": "(浮点数)- 该用户的最大预算",
"maxBudgetHelp": "该用户的最大预算。",
"resetBudgetLabel": "重置预算"
},
"emailSettings": {
"emailServerSettings": "邮件服务器设置",
"docsLink": "LiteLLM 文档:邮件告警",
"saveChanges": "保存更改",
"testEmailAlerts": "测试邮件告警",
"saveSuccess": "邮件设置已成功更新",
"testSuccess": "邮件测试已触发,请检查您配置的邮件收件箱/日志。",
"requiredStar": "必填 *",
"smtpHostDesc": "输入 SMTP 主机地址,例如 `smtp.resend.com`",
"smtpPortDesc": "输入 SMTP 端口号,例如 `587`",
"smtpUsernameDesc": "输入 SMTP 用户名,例如 `username`",
"smtpSenderEmailDesc": "输入发件人邮箱地址,例如 `sender@berri.ai`",
"testEmailAddressDesc": "发送「测试邮件告警」的目标邮箱,例如:`info@berri.ai`",
"emailLogoUrlDesc": "(可选)自定义邮件中显示的 Logo,传入您的 Logo URL",
"emailSupportContactDesc": "(可选)自定义邮件中显示的支持邮箱地址,默认为 support@berri.ai"
},
"generalSettings": {
"tabLoadbalancing": "负载均衡",
"tabRoutingGroups": "路由分组",
"tabFallbacks": "回退",
"tabGeneral": "常规",
"colSetting": "设置项",
"colValue": "值",
"colStatus": "状态",
"colAction": "操作",
"statusInDb": "数据库中",
"statusInConfig": "配置文件中",
"statusNotSet": "未设置"
},
"guardrails": {
"guardrailGardenTab": "护栏库",
"guardrailsTab": "护栏",
"testPlaygroundTab": "测试 Playground",
"submittedGuardrailsTab": "已提交的护栏",
"addNewGuardrail": "+ 添加新护栏",
"addProviderGuardrail": "添加提供商护栏",
"createCustomCodeGuardrail": "创建自定义代码护栏",
"deleteGuardrailTitle": "删除护栏",
"deleteGuardrailMessage": "确定要删除护栏:{{name}}?此操作无法撤销。",
"guardrailInfoTitle": "护栏信息",
"labelId": "ID",
"labelMode": "模式",
"labelDefaultOn": "默认启用",
"deleteSuccess": "护栏「{{name}}」已成功删除",
"deleteFailed": "删除护栏失败"
},
"keyValueInput": {
"headerNamePlaceholder": "请求头名称",
"headerValuePlaceholder": "请求头值",
"addHeader": "添加请求头"
},
"loggingSettingsView": {
"loggingIntegrations": "日志集成",
"parametersConfigured_one": "{{count}} 个参数已配置",
"parametersConfigured_other": "{{count}} 个参数已配置",
"noIntegrationsConfigured": "未配置日志集成",
"disabledCallbacks": "已禁用的回调",
"disabledForKey": "已为此密钥禁用",
"noCallbacksDisabled": "未禁用任何回调",
"cardTitle": "日志设置",
"cardSubtitle": "此密钥的活跃日志集成和已禁用的回调",
"inlineTitle": "日志设置",
"successOnly": "仅成功",
"failureOnly": "仅失败",
"successAndFailure": "成功与失败"
},
"mcpHubTableColumns": {
"colServerName": "服务器名称",
"colUrl": "URL",
"colTransport": "传输协议",
"colAuthType": "认证类型",
"colTools": "工具",
"colCreatedBy": "创建者",
"colPublic": "公开",
"copyServerName": "复制服务器名称",
"copyUrl": "复制 URL",
"toolCount_one": "{{count}} 个工具",
"toolCount_other": "{{count}} 个工具",
"allTools": "所有工具",
"infoShort": "详情"
},
"common": {
"save": "保存",
"cancel": "取消",