feat(ui): i18n for common_components batch 2

This commit is contained in:
吹雪 2026-06-10 11:59:23 +09:00
parent 77fbc36e94
commit c084e10de9
12 changed files with 342 additions and 103 deletions

View file

@ -1,5 +1,6 @@
import React from "react";
import { Select, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { Organization } from "../networking";
const { Text } = Typography;
@ -21,10 +22,12 @@ const OrganizationDropdown: React.FC<OrganizationDropdownProps> = ({
loading,
style,
}) => {
const { t } = useTranslation();
return (
<Select
showSearch
placeholder="All Organizations"
placeholder={t("commonComponents.organizationDropdown.placeholder")}
value={value}
onChange={onChange}
disabled={disabled}

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { Card, Title, Subtitle } from "@tremor/react";
import { Form, Select, Tooltip, Alert } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import GuardrailSelector from "../guardrails/GuardrailSelector";
interface PassThroughGuardrailsSectionProps {
@ -17,6 +18,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
onChange,
disabled = false,
}) => {
const { t } = useTranslation();
const [selectedGuardrails, setSelectedGuardrails] = useState<string[]>(Object.keys(value));
const [guardrailSettings, setGuardrailSettings] =
useState<Record<string, { request_fields?: string[]; response_fields?: string[] } | null>>(value);
@ -70,42 +72,41 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
return (
<Card className="p-6">
<Title className="text-lg font-semibold text-gray-900 mb-2">Guardrails</Title>
<Subtitle className="text-gray-600 mb-6">
Configure guardrails to enforce policies on requests and responses. Guardrails are opt-in for passthrough
endpoints.
</Subtitle>
<Title className="text-lg font-semibold text-gray-900 mb-2">
{t("commonComponents.passThroughGuardrailsSection.title")}
</Title>
<Subtitle className="text-gray-600 mb-6">{t("commonComponents.passThroughGuardrailsSection.subtitle")}</Subtitle>
<Alert
message={
<span>
Field-Level Targeting{" "}
{t("commonComponents.passThroughGuardrailsSection.fieldLevelTargeting")}{" "}
<a
href="https://docs.litellm.ai/docs/proxy/pass_through_guardrails#field-level-targeting"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
(Learn More)
({t("common.learnMore")})
</a>
</span>
}
description={
<div className="space-y-2">
<div>
Optionally specify which fields to check. If left empty, the entire request/response is sent to the
guardrail.
</div>
<div>{t("commonComponents.passThroughGuardrailsSection.fieldLevelDesc")}</div>
<div className="text-xs space-y-1 mt-2">
<div className="font-medium">Common Examples:</div>
<div className="font-medium">{t("commonComponents.passThroughGuardrailsSection.commonExamples")}</div>
<div>
• <code className="bg-gray-100 px-1 rounded">query</code> - Single field
• <code className="bg-gray-100 px-1 rounded">query</code> -{" "}
{t("commonComponents.passThroughGuardrailsSection.singleField")}
</div>
<div>
• <code className="bg-gray-100 px-1 rounded">documents[*].text</code> - All text in documents array
• <code className="bg-gray-100 px-1 rounded">documents[*].text</code> -{" "}
{t("commonComponents.passThroughGuardrailsSection.allTextInDocuments")}
</div>
<div>
• <code className="bg-gray-100 px-1 rounded">messages[*].content</code> - All message contents
• <code className="bg-gray-100 px-1 rounded">messages[*].content</code> -{" "}
{t("commonComponents.passThroughGuardrailsSection.allMessageContents")}
</div>
</div>
</div>
@ -118,8 +119,8 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Select Guardrails
<Tooltip title="Choose which guardrails should run on this endpoint. Org/team/key level guardrails will also be included.">
{t("commonComponents.passThroughGuardrailsSection.selectGuardrails")}
<Tooltip title={t("commonComponents.passThroughGuardrailsSection.selectGuardrailsTooltip")}>
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
@ -136,8 +137,12 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
{selectedGuardrails.length > 0 && (
<div className="mt-6 space-y-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium text-gray-700">Field Targeting (Optional)</div>
<div className="text-xs text-gray-500">💡 Tip: Leave empty to check entire payload</div>
<div className="text-sm font-medium text-gray-700">
{t("commonComponents.passThroughGuardrailsSection.fieldTargetingOptional")}
</div>
<div className="text-xs text-gray-500">
{t("commonComponents.passThroughGuardrailsSection.leaveEmptyTip")}
</div>
</div>
{selectedGuardrails.map((guardrailName) => (
<Card key={guardrailName} className="p-4 bg-gray-50">
@ -146,13 +151,15 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-gray-600 flex items-center">
Request Fields (pre_call)
{t("commonComponents.passThroughGuardrailsSection.requestFields")}
<Tooltip
title={
<div>
<div className="font-medium mb-1">Specify which request fields to check</div>
<div className="font-medium mb-1">
{t("commonComponents.passThroughGuardrailsSection.requestFieldsTooltipTitle")}
</div>
<div className="text-xs space-y-1">
<div>Examples:</div>
<div>{t("commonComponents.passThroughGuardrailsSection.examples")}</div>
<div>• query</div>
<div>• documents[*].text</div>
<div>• messages[*].content</div>
@ -191,7 +198,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Type field name or use + buttons above (e.g., query, documents[*].text)"
placeholder={t("commonComponents.passThroughGuardrailsSection.requestFieldsPlaceholder")}
value={guardrailSettings[guardrailName]?.request_fields || []}
onChange={(fields) => handleFieldChange(guardrailName, "request_fields", fields)}
disabled={disabled}
@ -201,13 +208,15 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-gray-600 flex items-center">
Response Fields (post_call)
{t("commonComponents.passThroughGuardrailsSection.responseFields")}
<Tooltip
title={
<div>
<div className="font-medium mb-1">Specify which response fields to check</div>
<div className="font-medium mb-1">
{t("commonComponents.passThroughGuardrailsSection.responseFieldsTooltipTitle")}
</div>
<div className="text-xs space-y-1">
<div>Examples:</div>
<div>{t("commonComponents.passThroughGuardrailsSection.examples")}</div>
<div>• results[*].text</div>
<div>• choices[*].message.content</div>
</div>
@ -234,7 +243,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Type field name or use + buttons above (e.g., results[*].text)"
placeholder={t("commonComponents.passThroughGuardrailsSection.responseFieldsPlaceholder")}
value={guardrailSettings[guardrailName]?.response_fields || []}
onChange={(fields) => handleFieldChange(guardrailName, "response_fields", fields)}
disabled={disabled}

View file

@ -1,6 +1,7 @@
import React from "react";
import { Card, Title, Subtitle, Text } from "@tremor/react";
import { Form, Switch } from "antd";
import { Trans, useTranslation } from "react-i18next";
export interface PassThroughSecuritySectionProps {
premiumUser: boolean;
@ -17,12 +18,14 @@ const PassThroughSecuritySection: React.FC<PassThroughSecuritySectionProps> = ({
authEnabled,
onAuthChange,
}) => {
const { t } = useTranslation();
return (
<Card className="p-6">
<Title className="text-lg font-semibold text-gray-900 mb-2">Security</Title>
<Subtitle className="text-gray-600 mb-4">
When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key
</Subtitle>
<Title className="text-lg font-semibold text-gray-900 mb-2">
{t("commonComponents.passThroughSecuritySection.title")}
</Title>
<Subtitle className="text-gray-600 mb-4">{t("commonComponents.passThroughSecuritySection.subtitle")}</Subtitle>
{premiumUser ? (
<Form.Item name="auth" valuePropName="checked" className="mb-0">
<Switch
@ -36,15 +39,25 @@ const PassThroughSecuritySection: React.FC<PassThroughSecuritySectionProps> = ({
<div>
<div className="flex items-center mb-3">
<Switch disabled checked={false} style={{ outline: "2px solid #d1d5db", outlineOffset: "2px" }} />
<span className="ml-2 text-sm text-gray-400">Authentication (Premium)</span>
<span className="ml-2 text-sm text-gray-400">
{t("commonComponents.passThroughSecuritySection.authenticationPremium")}
</span>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Text className="text-sm text-yellow-800">
Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
here
</a>
.
<Trans
i18nKey="commonComponents.passThroughSecuritySection.enterpriseNotice"
components={{
pricingLink: (
<a
href="https://www.litellm.ai/#pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
/>
),
}}
/>
</Text>
</div>
</div>

View file

@ -1,5 +1,6 @@
import React from "react";
import { Text } from "@tremor/react";
import { Trans, useTranslation } from "react-i18next";
import LoggingSettings from "../team/LoggingSettings";
interface PremiumLoggingSettingsProps {
@ -17,6 +18,8 @@ export function PremiumLoggingSettings({
disabledCallbacks = [],
onDisabledCallbacksChange,
}: PremiumLoggingSettingsProps) {
const { t } = useTranslation();
if (!premiumUser) {
return (
<div>
@ -30,12 +33,19 @@ export function PremiumLoggingSettings({
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Text className="text-sm text-yellow-800">
Setting Key/Team logging settings is a LiteLLM Enterprise feature. Global Logging Settings are available for
all free users. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
here
</a>
.
<Trans
i18nKey="commonComponents.premiumLoggingSettings.enterpriseNotice"
components={{
pricingLink: (
<a
href="https://www.litellm.ai/#pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
/>
),
}}
/>
</Text>
</div>
</div>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Select, Spin } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
interface ProjectDropdownProps {
@ -14,12 +15,13 @@ interface ProjectDropdownProps {
}
const ProjectDropdown: React.FC<ProjectDropdownProps> = ({ projects, value, onChange, disabled, loading, teamId }) => {
const { t } = useTranslation();
const filtered = teamId ? projects?.filter((p) => p.team_id === teamId) : projects;
return (
<Select
showSearch
placeholder="Search or select a project"
placeholder={t("commonComponents.projectDropdown.placeholder")}
value={value}
onChange={onChange}
disabled={disabled}

View file

@ -1,6 +1,7 @@
import React from "react";
import { Form, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
const { Option } = Select;
@ -30,6 +31,7 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
form,
onChange,
}) => {
const { t } = useTranslation();
const limitTypeUpper = type.toUpperCase();
const limitTypeLower = type.toLowerCase();
@ -42,13 +44,15 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
}
};
const tooltipTitle = `Select 'guaranteed_throughput' to prevent overallocating ${limitTypeUpper} limit when the key belongs to a Team with specific ${limitTypeUpper} limits.`;
const tooltipTitle = t("commonComponents.rateLimitTypeFormItem.tooltip", {
limitTypeUpper,
});
return (
<Form.Item
label={
<span>
{limitTypeUpper} Rate Limit Type{" "}
{t("commonComponents.rateLimitTypeFormItem.label", { limitTypeUpper })}{" "}
<Tooltip title={tooltipTitle}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
@ -60,46 +64,52 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
>
<Select
defaultValue={showDetailedDescriptions ? "default" : undefined}
placeholder="Select rate limit type"
placeholder={t("commonComponents.rateLimitTypeFormItem.selectPlaceholder")}
style={{ width: "100%" }}
optionLabelProp={showDetailedDescriptions ? "label" : undefined}
onChange={handleChange}
>
{showDetailedDescriptions ? (
<>
<Option value="best_effort_throughput" label="Default">
<Option value="best_effort_throughput" label={t("common.default")}>
<div style={{ padding: "4px 0" }}>
<div style={{ fontWeight: 500 }}>Default</div>
<div style={{ fontWeight: 500 }}>{t("common.default")}</div>
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
Best effort throughput - no error if we&apos;re overallocating {limitTypeLower} (Team/Key Limits
checked at runtime).
{t("commonComponents.rateLimitTypeFormItem.bestEffortDesc", { limitTypeLower })}
</div>
</div>
</Option>
<Option value="guaranteed_throughput" label="Guaranteed throughput">
<Option
value="guaranteed_throughput"
label={t("commonComponents.rateLimitTypeFormItem.guaranteedThroughput")}
>
<div style={{ padding: "4px 0" }}>
<div style={{ fontWeight: 500 }}>Guaranteed throughput</div>
<div style={{ fontWeight: 500 }}>
{t("commonComponents.rateLimitTypeFormItem.guaranteedThroughput")}
</div>
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
Guaranteed throughput - raise an error if we&apos;re overallocating {limitTypeLower} (also checks
model-specific limits)
{t("commonComponents.rateLimitTypeFormItem.guaranteedThroughputDesc", { limitTypeLower })}
</div>
</div>
</Option>
<Option value="dynamic" label="Dynamic">
<Option value="dynamic" label={t("commonComponents.rateLimitTypeFormItem.dynamic")}>
<div style={{ padding: "4px 0" }}>
<div style={{ fontWeight: 500 }}>Dynamic</div>
<div style={{ fontWeight: 500 }}>{t("commonComponents.rateLimitTypeFormItem.dynamic")}</div>
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
If the key has a set {limitTypeUpper} (e.g. 2 {limitTypeUpper}) and there are no 429 errors, it can
dynamically exceed the limit when the model being called is not erroring.
{t("commonComponents.rateLimitTypeFormItem.dynamicDesc", { limitTypeUpper })}
</div>
</div>
</Option>
</>
) : (
<>
<Option value="best_effort_throughput">Best effort throughput</Option>
<Option value="guaranteed_throughput">Guaranteed throughput</Option>
<Option value="dynamic">Dynamic</Option>
<Option value="best_effort_throughput">
{t("commonComponents.rateLimitTypeFormItem.bestEffortThroughput")}
</Option>
<Option value="guaranteed_throughput">
{t("commonComponents.rateLimitTypeFormItem.guaranteedThroughput")}
</Option>
<Option value="dynamic">{t("commonComponents.rateLimitTypeFormItem.dynamic")}</Option>
</>
)}
</Select>

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { getRouterSettingsCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "../router_settings/RouterSettingsForm";
import { Fallbacks } from "../Settings/RouterSettings/Fallbacks/AddFallbacks";
@ -37,6 +38,7 @@ export interface RouterSettingsAccordionRef {
const RouterSettingsAccordion = forwardRef<RouterSettingsAccordionRef, RouterSettingsAccordionProps>(
({ accessToken, value, onChange, modelData }, ref) => {
const { t } = useTranslation();
const [formValue, setFormValue] = useState<RouterSettingsFormValue>({
routerSettings: {},
selectedStrategy: null,
@ -347,8 +349,8 @@ const RouterSettingsAccordion = forwardRef<RouterSettingsAccordionRef, RouterSet
<div className="w-full">
<TabGroup className="w-full">
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Fallbacks</Tab>
<Tab value="1">{t("commonComponents.routerSettingsAccordion.loadbalancing")}</Tab>
<Tab value="2">{t("commonComponents.routerSettingsAccordion.fallbacks")}</Tab>
</TabList>
<TabPanels className="px-8 py-6">
<TabPanel>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Button, Dropdown, MenuProps } from "antd";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, XIcon } from "@heroicons/react/outline";
import { useTranslation } from "react-i18next";
export type SortState = "asc" | "desc" | false;
@ -21,6 +22,8 @@ interface TableHeaderSortDropdownProps {
}
export const TableHeaderSortDropdown: React.FC<TableHeaderSortDropdownProps> = ({ sortState, onSortChange }) => {
const { t } = useTranslation();
const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
if (key === "asc") {
onSortChange("asc");
@ -34,17 +37,17 @@ export const TableHeaderSortDropdown: React.FC<TableHeaderSortDropdownProps> = (
const menuItems: MenuProps["items"] = [
{
key: "asc",
label: "Ascending",
label: t("commonComponents.tableHeaderSortDropdown.ascending"),
icon: <ChevronUpIcon className="h-4 w-4" />,
},
{
key: "desc",
label: "Descending",
label: t("commonComponents.tableHeaderSortDropdown.descending"),
icon: <ChevronDownIcon className="h-4 w-4" />,
},
{
key: "reset",
label: "Reset",
label: t("common.reset"),
icon: <XIcon className="h-4 w-4" />,
},
];

View file

@ -3,6 +3,8 @@ import { Form, Input, InputNumber, Select } from "antd";
import { TextInput } from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Tooltip } from "antd";
import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { getOpenAPISchema } from "../networking";
import { formatLabel } from "@/utils/textUtils";
@ -53,44 +55,42 @@ const validateJSON = (value: string): boolean => {
}
};
const getFieldHelp = (key: string, property: SchemaProperty, type: string): string => {
// Default help text based on type
const getFieldHelp = (key: string, property: SchemaProperty, type: string, t: TFunction): string => {
const defaultHelp =
{
string: "Text input",
number: "Numeric input",
integer: "Whole number input",
boolean: "True/False value",
}[type] || "Text input";
string: t("commonComponents.checkOpenapiSchema.helpText"),
number: t("commonComponents.checkOpenapiSchema.helpNumber"),
integer: t("commonComponents.checkOpenapiSchema.helpInteger"),
boolean: t("commonComponents.checkOpenapiSchema.helpBoolean"),
}[type] || t("commonComponents.checkOpenapiSchema.helpText");
// Specific field help text
const specificHelp: { [key: string]: string } = {
max_budget: "Enter maximum budget in USD (e.g., 100.50)",
budget_duration: "Select a time period for budget reset",
tpm_limit: "Enter maximum tokens per minute (whole number)",
rpm_limit: "Enter maximum requests per minute (whole number)",
duration: "Enter duration (e.g., 30s, 24h, 7d)",
metadata: 'Enter JSON object with key-value pairs\nExample: {"team": "research", "project": "nlp"}',
config: 'Enter configuration as JSON object\nExample: {"setting": "value"}',
permissions: "Enter comma-separated permission strings",
enforced_params: 'Enter parameters as JSON object\nExample: {"param": "value"}',
blocked: "Enter true/false or specific block conditions",
aliases: 'Enter aliases as JSON object\nExample: {"alias1": "value1", "alias2": "value2"}',
models: "Select one or more model names",
key_alias: "Enter a unique identifier for this key",
tags: "Enter comma-separated tag strings",
max_budget: t("commonComponents.checkOpenapiSchema.helpMaxBudget"),
budget_duration: t("commonComponents.checkOpenapiSchema.helpBudgetDuration"),
tpm_limit: t("commonComponents.checkOpenapiSchema.helpTpmLimit"),
rpm_limit: t("commonComponents.checkOpenapiSchema.helpRpmLimit"),
duration: t("commonComponents.checkOpenapiSchema.helpDuration"),
metadata: t("commonComponents.checkOpenapiSchema.helpMetadata"),
config: t("commonComponents.checkOpenapiSchema.helpConfig"),
permissions: t("commonComponents.checkOpenapiSchema.helpPermissions"),
enforced_params: t("commonComponents.checkOpenapiSchema.helpEnforcedParams"),
blocked: t("commonComponents.checkOpenapiSchema.helpBlocked"),
aliases: t("commonComponents.checkOpenapiSchema.helpAliases"),
models: t("commonComponents.checkOpenapiSchema.helpModels"),
key_alias: t("commonComponents.checkOpenapiSchema.helpKeyAlias"),
tags: t("commonComponents.checkOpenapiSchema.helpTags"),
};
// Get specific help text or use default based on type
const helpText = specificHelp[key] || defaultHelp;
// Add format requirements for special cases
if (isJSONField(key, property)) {
return `${helpText}\nMust be valid JSON format`;
return `${helpText}\n${t("commonComponents.checkOpenapiSchema.mustBeValidJson")}`;
}
if (property.enum) {
return `Select from available options\nAllowed values: ${property.enum.join(", ")}`;
return t("commonComponents.checkOpenapiSchema.selectFromOptions", {
values: property.enum.join(", "),
});
}
return helpText;
@ -105,6 +105,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
customValidation = {},
defaultValues = {},
}) => {
const { t } = useTranslation();
const [schemaProperties, setSchemaProperties] = useState<OpenAPISchema | null>(null);
const [error, setError] = useState<string | null>(null);
@ -130,7 +131,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
form.setFieldsValue(defaultFormValues);
} catch (error) {
console.error("Schema fetch error:", error);
setError(error instanceof Error ? error.message : "Failed to fetch schema");
setError(error instanceof Error ? error.message : t("commonComponents.checkOpenapiSchema.fetchFailed"));
}
};
@ -158,7 +159,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
const rules = [];
if (isRequired) {
rules.push({ required: true, message: `${label} is required` });
rules.push({ required: true, message: t("commonComponents.checkOpenapiSchema.fieldRequired", { label }) });
}
if (customValidation[key]) {
rules.push({ validator: customValidation[key] });
@ -167,7 +168,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
rules.push({
validator: async (_: any, value: string) => {
if (value && !validateJSON(value)) {
throw new Error("Please enter valid JSON");
throw new Error(t("commonComponents.checkOpenapiSchema.invalidJson"));
}
},
});
@ -186,7 +187,13 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
let inputComponent;
if (isJSONField(key, property)) {
inputComponent = <Input.TextArea rows={4} placeholder="Enter as JSON" className="font-mono" />;
inputComponent = (
<Input.TextArea
rows={4}
placeholder={t("commonComponents.checkOpenapiSchema.enterAsJson")}
className="font-mono"
/>
);
} else if (property.enum) {
inputComponent = (
<Select>
@ -200,7 +207,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
} else if (type === "number" || type === "integer") {
inputComponent = <InputNumber style={{ width: "100%" }} precision={type === "integer" ? 0 : undefined} />;
} else if (key === "duration") {
inputComponent = <TextInput placeholder="eg: 30s, 30h, 30d" />;
inputComponent = <TextInput placeholder={t("commonComponents.checkOpenapiSchema.durationPlaceholder")} />;
} else {
inputComponent = <TextInput placeholder={tooltip || ""} />;
}
@ -213,7 +220,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
className="mt-8"
rules={rules}
initialValue={defaultValues[key]}
help={<div className="text-xs text-gray-500">{getFieldHelp(key, property, type)}</div>}
help={<div className="text-xs text-gray-500">{getFieldHelp(key, property, type, t)}</div>}
>
{inputComponent}
</Form.Item>
@ -221,7 +228,11 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
};
if (error) {
return <div className="text-red-500">Error: {error}</div>;
return (
<div className="text-red-500">
{t("commonComponents.checkOpenapiSchema.errorPrefix")} {error}
</div>
);
}
if (!schemaProperties?.properties) {

View file

@ -1,6 +1,7 @@
import React, { useMemo, useState, type UIEvent } from "react";
import { Select, Typography } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { Team } from "../key_team_helpers/key_list";
@ -29,6 +30,7 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({
organizationId,
pageSize = 20,
}) => {
const { t } = useTranslation();
const [searchInput, setSearchInput] = useState("");
const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", {
wait: DEBOUNCE_MS,
@ -78,7 +80,7 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({
return (
<Select
showSearch
placeholder="Search or select a team"
placeholder={t("commonComponents.teamDropdown.placeholder")}
value={value || undefined}
onChange={handleChange}
disabled={disabled}
@ -88,7 +90,7 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({
searchValue={searchInput}
onPopupScroll={handlePopupScroll}
loading={isLoading}
notFoundContent={isLoading ? <LoadingOutlined spin /> : "No teams found"}
notFoundContent={isLoading ? <LoadingOutlined spin /> : t("commonComponents.teamDropdown.notFound")}
data-testid="team-dropdown"
popupRender={(menu) => (
<>

View file

@ -278,6 +278,93 @@
"labelText": "Select Model",
"enterCustomModel": "Enter custom model",
"customModelPlaceholder": "Enter custom model name"
},
"organizationDropdown": {
"placeholder": "All Organizations"
},
"passThroughGuardrailsSection": {
"title": "Guardrails",
"subtitle": "Configure guardrails to enforce policies on requests and responses. Guardrails are opt-in for passthrough endpoints.",
"fieldLevelTargeting": "Field-Level Targeting",
"fieldLevelDesc": "Optionally specify which fields to check. If left empty, the entire request/response is sent to the guardrail.",
"commonExamples": "Common Examples:",
"singleField": "Single field",
"allTextInDocuments": "All text in documents array",
"allMessageContents": "All message contents",
"selectGuardrails": "Select Guardrails",
"selectGuardrailsTooltip": "Choose which guardrails should run on this endpoint. Org/team/key level guardrails will also be included.",
"fieldTargetingOptional": "Field Targeting (Optional)",
"leaveEmptyTip": "Tip: Leave empty to check entire payload",
"requestFields": "Request Fields (pre_call)",
"requestFieldsTooltipTitle": "Specify which request fields to check",
"responseFields": "Response Fields (post_call)",
"responseFieldsTooltipTitle": "Specify which response fields to check",
"examples": "Examples:",
"requestFieldsPlaceholder": "Type field name or use + buttons above (e.g., query, documents[*].text)",
"responseFieldsPlaceholder": "Type field name or use + buttons above (e.g., results[*].text)"
},
"passThroughSecuritySection": {
"title": "Security",
"subtitle": "When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key",
"authenticationPremium": "Authentication (Premium)",
"enterpriseNotice": "Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key <pricingLink>here</pricingLink>."
},
"premiumLoggingSettings": {
"enterpriseNotice": "Setting Key/Team logging settings is a LiteLLM Enterprise feature. Global Logging Settings are available for all free users. Get a trial key <pricingLink>here</pricingLink>."
},
"projectDropdown": {
"placeholder": "Search or select a project"
},
"rateLimitTypeFormItem": {
"label": "{{limitTypeUpper}} Rate Limit Type",
"tooltip": "Select 'guaranteed_throughput' to prevent overallocating {{limitTypeUpper}} limit when the key belongs to a Team with specific {{limitTypeUpper}} limits.",
"selectPlaceholder": "Select rate limit type",
"bestEffortThroughput": "Best effort throughput",
"bestEffortDesc": "Best effort throughput - no error if we're overallocating {{limitTypeLower}} (Team/Key Limits checked at runtime).",
"guaranteedThroughput": "Guaranteed throughput",
"guaranteedThroughputDesc": "Guaranteed throughput - raise an error if we're overallocating {{limitTypeLower}} (also checks model-specific limits)",
"dynamic": "Dynamic",
"dynamicDesc": "If the key has a set {{limitTypeUpper}} (e.g. 2 {{limitTypeUpper}}) and there are no 429 errors, it can dynamically exceed the limit when the model being called is not erroring."
},
"routerSettingsAccordion": {
"loadbalancing": "Loadbalancing",
"fallbacks": "Fallbacks"
},
"tableHeaderSortDropdown": {
"ascending": "Ascending",
"descending": "Descending"
},
"checkOpenapiSchema": {
"helpText": "Text input",
"helpNumber": "Numeric input",
"helpInteger": "Whole number input",
"helpBoolean": "True/False value",
"helpMaxBudget": "Enter maximum budget in USD (e.g., 100.50)",
"helpBudgetDuration": "Select a time period for budget reset",
"helpTpmLimit": "Enter maximum tokens per minute (whole number)",
"helpRpmLimit": "Enter maximum requests per minute (whole number)",
"helpDuration": "Enter duration (e.g., 30s, 24h, 7d)",
"helpMetadata": "Enter JSON object with key-value pairs\nExample: {\"team\": \"research\", \"project\": \"nlp\"}",
"helpConfig": "Enter configuration as JSON object\nExample: {\"setting\": \"value\"}",
"helpPermissions": "Enter comma-separated permission strings",
"helpEnforcedParams": "Enter parameters as JSON object\nExample: {\"param\": \"value\"}",
"helpBlocked": "Enter true/false or specific block conditions",
"helpAliases": "Enter aliases as JSON object\nExample: {\"alias1\": \"value1\", \"alias2\": \"value2\"}",
"helpModels": "Select one or more model names",
"helpKeyAlias": "Enter a unique identifier for this key",
"helpTags": "Enter comma-separated tag strings",
"mustBeValidJson": "Must be valid JSON format",
"selectFromOptions": "Select from available options\nAllowed values: {{values}}",
"fieldRequired": "{{label}} is required",
"invalidJson": "Please enter valid JSON",
"enterAsJson": "Enter as JSON",
"durationPlaceholder": "eg: 30s, 30h, 30d",
"fetchFailed": "Failed to fetch schema",
"errorPrefix": "Error:"
},
"teamDropdown": {
"placeholder": "Search or select a team",
"notFound": "No teams found"
}
},
"common": {

View file

@ -278,6 +278,93 @@
"labelText": "选择模型",
"enterCustomModel": "输入自定义模型",
"customModelPlaceholder": "输入自定义模型名称"
},
"organizationDropdown": {
"placeholder": "所有组织"
},
"passThroughGuardrailsSection": {
"title": "护栏",
"subtitle": "配置护栏以对请求和响应执行策略。护栏对透传端点为按需启用。",
"fieldLevelTargeting": "字段级定向",
"fieldLevelDesc": "可选择性指定要检查的字段。若留空,整个请求/响应将发送给护栏。",
"commonExamples": "常见示例:",
"singleField": "单个字段",
"allTextInDocuments": "文档数组中的所有文本",
"allMessageContents": "所有消息内容",
"selectGuardrails": "选择护栏",
"selectGuardrailsTooltip": "选择应在此端点运行的护栏。组织/团队/密钥级别的护栏也将被包含。",
"fieldTargetingOptional": "字段定向(可选)",
"leaveEmptyTip": "提示:留空则检查整个载荷",
"requestFields": "请求字段 (pre_call)",
"requestFieldsTooltipTitle": "指定要检查的请求字段",
"responseFields": "响应字段 (post_call)",
"responseFieldsTooltipTitle": "指定要检查的响应字段",
"examples": "示例:",
"requestFieldsPlaceholder": "输入字段名或使用上方 + 按钮(如:query、documents[*].text)",
"responseFieldsPlaceholder": "输入字段名或使用上方 + 按钮(如:results[*].text)"
},
"passThroughSecuritySection": {
"title": "安全",
"subtitle": "启用后,发往此端点的请求将需要有效的 LiteLLM 虚拟密钥",
"authenticationPremium": "认证(高级版)",
"enterpriseNotice": "为透传端点设置认证是 LiteLLM 企业版功能。获取试用密钥请访问<pricingLink>此处</pricingLink>。"
},
"premiumLoggingSettings": {
"enterpriseNotice": "设置密钥/团队日志配置是 LiteLLM 企业版功能。所有免费用户均可使用全局日志设置。获取试用密钥请访问<pricingLink>此处</pricingLink>。"
},
"projectDropdown": {
"placeholder": "搜索或选择项目"
},
"rateLimitTypeFormItem": {
"label": "{{limitTypeUpper}} 速率限制类型",
"tooltip": "选择 guaranteed_throughput 可防止在密钥属于具有特定 {{limitTypeUpper}} 限制的团队时过度分配 {{limitTypeUpper}} 限额。",
"selectPlaceholder": "选择速率限制类型",
"bestEffortThroughput": "尽力吞吐",
"bestEffortDesc": "尽力吞吐 — 过度分配 {{limitTypeLower}} 时不报错(运行时检查团队/密钥限制)。",
"guaranteedThroughput": "保障吞吐",
"guaranteedThroughputDesc": "保障吞吐 — 过度分配 {{limitTypeLower}} 时报错(同时检查模型专属限制)",
"dynamic": "动态",
"dynamicDesc": "若密钥设置了 {{limitTypeUpper}}(例如 2 {{limitTypeUpper}})且无 429 错误,则当被调用的模型未报错时可动态超出限制。"
},
"routerSettingsAccordion": {
"loadbalancing": "负载均衡",
"fallbacks": "回退"
},
"tableHeaderSortDropdown": {
"ascending": "升序",
"descending": "降序"
},
"checkOpenapiSchema": {
"helpText": "文本输入",
"helpNumber": "数值输入",
"helpInteger": "整数输入",
"helpBoolean": "True/False 值",
"helpMaxBudget": "输入最大预算(美元,例如:100.50)",
"helpBudgetDuration": "选择预算重置的时间周期",
"helpTpmLimit": "输入每分钟最大 Token 数(整数)",
"helpRpmLimit": "输入每分钟最大请求数(整数)",
"helpDuration": "输入时长(例如:30s、24h、7d)",
"helpMetadata": "输入键值对 JSON 对象\n示例:{\"team\": \"research\", \"project\": \"nlp\"}",
"helpConfig": "以 JSON 对象输入配置\n示例:{\"setting\": \"value\"}",
"helpPermissions": "输入逗号分隔的权限字符串",
"helpEnforcedParams": "以 JSON 对象输入参数\n示例:{\"param\": \"value\"}",
"helpBlocked": "输入 true/false 或具体的屏蔽条件",
"helpAliases": "以 JSON 对象输入别名\n示例:{\"alias1\": \"value1\", \"alias2\": \"value2\"}",
"helpModels": "选择一个或多个模型名称",
"helpKeyAlias": "为此密钥输入唯一标识符",
"helpTags": "输入逗号分隔的标签字符串",
"mustBeValidJson": "必须为有效的 JSON 格式",
"selectFromOptions": "从可用选项中选择\n允许的值:{{values}}",
"fieldRequired": "{{label}} 为必填项",
"invalidJson": "请输入有效的 JSON",
"enterAsJson": "以 JSON 格式输入",
"durationPlaceholder": "例如:30s、30h、30d",
"fetchFailed": "获取 schema 失败",
"errorPrefix": "错误:"
},
"teamDropdown": {
"placeholder": "搜索或选择团队",
"notFound": "未找到团队"
}
},
"common": {