refactor(ui): clear hardcoded English from dashboard JSX (#50575)

* refactor(ui): clear hardcoded English from dashboard JSX

Move the 283 strings the planned no-literal-string guard flags so main
starts at zero:

- missed UI text (Switchyard router form, Create User custom ID field,
  advanced settings placeholders) and our own fallbacks ("unknown",
  seconds suffixes) onto t() with new en.json keys
- product names into src/i18n/untranslated/productNames.ts and code
  samples, sample values and JSON placeholders into
  src/i18n/untranslated/codeSamples.ts
- values that never display (filter ids, enum fallbacks, query keys,
  CSS, ids) onto literal-union types or module constants

The rendered English is byte-identical.

* refactor(ui): put Bedrock policy chips and test panel error on t()

The six Bedrock policy chips and the compliance test panel's error word are our own display labels, not server values, so they go through t() instead of module constants. Rendered English is unchanged.
This commit is contained in:
yuneng-jiang 2026-11-07 16:42:38 -08:00 • committed by GitHub
parent 7617e66acf
commit c22dc25316
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
117 changed files with 746 additions and 304 deletions

View file

@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AgentFormField, type AgentFormValues } from "./AgentFormKit";
import { entraTenantFromIssuer, IDENTITY_UUID_PATTERN } from "./agent_identity";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const PROVIDER_OPTIONS: { value: string; labelKey: ParseKeys }[] = [
{ value: "none", labelKey: "agents.identityFields.providers.none" },
@ -206,7 +207,7 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul
ref={ref}
value={typeof value === "string" ? value : ""}
onChange={onChange}
placeholder="Agent.Invoke"
placeholder={codeSamples.agents.identityScope}
/>
)}
</AgentFormField>

View file

@ -11,6 +11,7 @@ import { PasswordInput } from "@/components/shared/PasswordInput";
import i18n from "@/i18n/i18n";
import { AgentFormField, AgentFormValues, labelWithHint } from "./AgentFormKit";
import { KILL_SWITCH_AUTH_TYPES, KILL_SWITCH_METHODS, validateKillSwitchBody } from "./kill_switch_config";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const KeyValueFieldArray = ({
name,
@ -135,7 +136,7 @@ const KillSwitchAuthFields = () => {
<TextField
name="kill_switch.auth_header_name"
label={t("agents.killSwitchFields.auth.headerName")}
placeholder="X-API-Key"
placeholder={codeSamples.agents.killSwitchAuthHeader}
/>
<TextField
name="kill_switch.auth_api_key"
@ -224,7 +225,7 @@ const KillSwitchFormFields = () => (
{...control}
ref={ref}
rows={4}
placeholder='{"reason": "manual kill switch"}'
placeholder={codeSamples.agents.killSwitchPayloadJson}
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>

View file

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import CodeBlock from "@/components/CodeBlock";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DocLink from "./DocLink";
import { productNames } from "@/i18n/untranslated/productNames";
interface ApiRefProps {
proxySettings: {
@ -36,13 +37,13 @@ const APIReferenceView: React.FC<ApiRefProps> = ({ proxySettings }) => {
<Tabs defaultValue="openai">
<TabsList variant="line" className="border-b rounded-none w-full justify-start h-auto p-0">
<TabsTrigger value="openai" className="rounded-none px-4 py-2 flex-none">
OpenAI Python SDK
{productNames.openAIPythonSdk}
</TabsTrigger>
<TabsTrigger value="llamaindex" className="rounded-none px-4 py-2 flex-none">
LlamaIndex
{productNames.llamaIndex}
</TabsTrigger>
<TabsTrigger value="langchain" className="rounded-none px-4 py-2 flex-none">
Langchain Py
{productNames.langchainPython}
</TabsTrigger>
</TabsList>
<TabsContent value="openai" keepMounted>

View file

@ -5,6 +5,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CheckCircle2, XCircle, ClipboardCopy } from "lucide-react";
import { ResponseTimeIndicator } from "./response_time_indicator";
const UNHEALTHY_STATUS = "unhealthy";
// Helper function to deep-parse a JSON string if possible
const deepParse = (input: any) => {
let parsed = input;
@ -179,7 +181,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
className={`text-sm font-medium ${response?.status === "healthy" ? "text-success" : "text-destructive"}`}
>
{t("caching.health.cacheStatus")}
{response?.status || "unhealthy"}
{response?.status || UNHEALTHY_STATUS}
</p>
</div>

View file

@ -14,6 +14,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const DEFAULT_PROMPT = `Evaluate whether this guardrail's decision was correct.
Analyze the user input, the guardrail action taken, and determine if it was appropriate.
@ -130,7 +131,7 @@ export function EvaluationSettingsModal({
<label htmlFor="evaluation-schema" className="mb-1.5 block text-sm font-medium text-foreground">
{t("guardrailsMonitor.evaluationSettings.schema.label")}
</label>
<p className="mb-1 text-xs text-muted-foreground">response_format: json_schema</p>
<p className="mb-1 text-xs text-muted-foreground">{codeSamples.guardrails.evaluationResponseFormat}</p>
<Textarea
id="evaluation-schema"
value={schema}

View file

@ -5,6 +5,8 @@ import { GuardrailModeRows } from "./GuardrailModeDisplay";
import { GuardrailStreamScopeDetail } from "./StreamScopeFields";
import ToolPermissionRulesEditor, { type ToolPermissionConfig } from "./tool_permission/ToolPermissionRulesEditor";
const DEFAULT_CHECK_ACTION = "block";
interface DecisionModelCheckView {
name: string;
action?: string;
@ -35,7 +37,7 @@ const DecisionModelSection = ({
<div key={check.name} className="flex items-center border-b border-border px-4 py-2 text-sm last:border-b-0">
<span className="flex-1">{check.name.replace(/_/g, " ")}</span>
<span className="w-24 text-right">
<Badge variant="secondary">{check.action ?? "block"}</Badge>
<Badge variant="secondary">{check.action ?? DEFAULT_CHECK_ACTION}</Badge>
</span>
<span className="w-24 text-right">{check.threshold ?? DEFAULT_DECISION_THRESHOLD}</span>
</div>

View file

@ -8,6 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import GuardrailTestResults from "./GuardrailTestResults";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface GuardrailTestPanelProps {
guardrailNames: string[];
@ -205,7 +206,7 @@ export function GuardrailTestPanel({
setMetadataError(parseMetadata(e.target.value).error);
}
}}
placeholder='{"forbidden_topics": ["tax", "finance"]}'
placeholder={codeSamples.guardrails.testPanelParamsJson}
rows={3}
className="font-mono text-sm field-sizing-fixed"
aria-invalid={metadataError ? true : undefined}

View file

@ -42,6 +42,7 @@ import { isValidUrl } from "@/lib/forms/urlValidation";
import { useZodForm } from "@/lib/forms/useZodForm";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const GUARDRAIL_MODES: { value: string; labelKey: ParseKeys }[] = [
{ value: "pre_call", labelKey: "guardrails.teamTab.modes.preCall" },
@ -1210,7 +1211,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
ref={ref}
rows={3}
className="font-mono text-xs"
placeholder='{"forward_api_key": true, "headers": {"X-Custom": "value"}}'
placeholder={codeSamples.guardrails.teamGuardrailParamsJson}
/>
)}
</FormField>
@ -1225,7 +1226,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
ref={ref}
rows={3}
className="font-mono text-xs"
placeholder='{"description": "Detects PII in requests"}'
placeholder={codeSamples.guardrails.teamGuardrailInfoJson}
/>
)}
</FormField>

View file

@ -205,6 +205,7 @@ const ALWAYS_ON_ITEMS: { labelKey: ParseKeys; value: boolean }[] = [
];
const DEFAULT_MODES = ["pre_call", "during_call", "post_call", "logging_only"];
const REALTIME_VIOLATION_ACTIONS = ["warn", "end_session"] as const;
const CALL_TYPE_ITEMS = [{ label: "/v1/realtime", value: "realtime" }];
@ -1171,7 +1172,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
{t("guardrails.addForm.endpoint.realtime.onViolation.label")}
</label>
<div className="space-y-2">
{(["warn", "end_session"] as const).map((opt) => (
{REALTIME_VIOLATION_ACTIONS.map((opt) => (
<label key={opt} className="flex items-start gap-2 cursor-pointer">
<input
type="radio"

View file

@ -19,6 +19,8 @@ import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ACTION_ITEMS, SEVERITY_ITEMS, translateOptions } from "./action_options";
const DEFAULT_CATEGORY_FILE_TYPE = "yaml";
interface ContentCategory {
name: string;
display_name: string;
@ -39,7 +41,7 @@ interface ContentCategoryConfigurationProps {
selectedCategories: SelectedCategory[];
onCategoryAdd: (category: SelectedCategory) => void;
onCategoryRemove: (id: string) => void;
onCategoryUpdate: (id: string, field: string, value: any) => void;
onCategoryUpdate: (id: string, field: "action" | "severity_threshold", value: any) => void;
accessToken?: string | null;
pendingSelection?: string;
onPendingSelectionChange?: (value: string) => void;
@ -123,7 +125,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
}
setCategoryYaml((prev) => ({ ...prev, [categoryName]: content }));
setCategoryFileTypes((prev) => ({ ...prev, [categoryName]: data.file_type || "yaml" }));
setCategoryFileTypes((prev) => ({ ...prev, [categoryName]: data.file_type || DEFAULT_CATEGORY_FILE_TYPE }));
} catch (error) {
console.error(`Failed to fetch content for category ${categoryName}:`, error);
} finally {
@ -160,7 +162,10 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
setPreviewYaml(content);
// Also cache it for later use
setCategoryYaml((prev) => ({ ...prev, [selectedCategoryName]: content }));
setCategoryFileTypes((prev) => ({ ...prev, [selectedCategoryName]: data.file_type || "yaml" }));
setCategoryFileTypes((prev) => ({
...prev,
[selectedCategoryName]: data.file_type || DEFAULT_CATEGORY_FILE_TYPE,
}));
})
.catch((error) => {
console.error(`Failed to fetch preview content for category ${selectedCategoryName}:`, error);
@ -339,7 +344,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
<DataTable data={selectedCategories} columns={columns} getRowId={(row) => row.id} size="compact" />
<div className="mt-4 space-y-2">
{selectedCategories.map((category) => {
const fileType = categoryFileTypes[category.category] || "yaml";
const fileType = categoryFileTypes[category.category] || DEFAULT_CATEGORY_FILE_TYPE;
const isExpanded = expandedYamlCategories.includes(category.category);
return (

View file

@ -16,7 +16,7 @@ interface BlockedWord {
interface KeywordTableProps {
keywords: BlockedWord[];
onActionChange: (id: string, field: string, value: any) => void;
onActionChange: (id: string, field: "action", value: any) => void;
onRemove: (id: string) => void;
}

View file

@ -54,6 +54,7 @@ import {
TEMPLATE_ITEMS,
type ModeOption,
} from "./custom_code_catalog";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
// Data for editing an existing guardrail
@ -696,7 +697,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
i18nKey="guardrails.customCode.test.fields.structuredMessages"
components={{ field: <strong>structured_messages</strong> }}
/>{" "}
<span className="text-warning">(pre_call)</span>
<span className="text-warning">{codeSamples.guardrails.customCodeHook}</span>
</div>
<div>
<Trans
@ -711,7 +712,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
onChange={(e) => setTestInput(e.target.value)}
rows={8}
className="font-mono text-xs field-sizing-fixed"
placeholder='{"texts": ["test message"], ...}'
placeholder={codeSamples.guardrails.customCodeInputJson}
/>
</div>
<div className="flex items-center gap-3">

View file

@ -27,7 +27,8 @@ const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }>
<div className="mt-2.5 flex items-center gap-1 text-success">
<CircleCheck className="size-3" />
<span className="text-[11px] font-medium">
F1: {card.eval.f1}% &middot; {card.eval.testCases} {t("guardrails.gardenCard.testCases")}
{t("guardrails.gardenCard.f1Label")}
{card.eval.f1}% &middot; {card.eval.testCases} {t("guardrails.gardenCard.testCases")}
</span>
</div>
)}

View file

@ -9,6 +9,7 @@ import AddGuardrailForm from "./add_guardrail_form";
import { Logo } from "@/components/molecules/logo/Logo";
import { GUARDRAIL_PRESETS } from "./guardrail_garden_configs";
import { GuardrailCardInfo, gardenCardDescription, gardenCardName, gardenTagLabel } from "./guardrail_garden_data";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const CATEGORY_LABEL_KEYS: Record<GuardrailCardInfo["category"], { provider: ParseKeys; type: ParseKeys }> = {
litellm: {
@ -180,7 +181,10 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
{/* Guardrail ID */}
<div className="mb-7">
<div className="mb-1 text-xs text-muted-foreground">{t("guardrails.gardenDetail.guardrailIdLabel")}</div>
<div className="break-all text-[13px] text-foreground">litellm/{card.id}</div>
<div className="break-all text-[13px] text-foreground">
{codeSamples.guardrails.gardenIdPrefix}
{card.id}
</div>
</div>
{/* Type */}

View file

@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
export type ToolPermissionDecision = "allow" | "deny";
export type ToolPermissionDefaultAction = "allow" | "deny";
@ -154,13 +155,13 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<div key={`${rule.id || index}-${patternIndex}`} className="flex items-start gap-2">
<Input
disabled={disabled}
placeholder="messages[0].content"
placeholder={codeSamples.guardrails.toolParamPath}
value={path}
onChange={(e) => updateAllowedParamPath(index, patternIndex, e.target.value)}
/>
<Input
disabled={disabled}
placeholder="^email@.*$"
placeholder={codeSamples.guardrails.toolParamPattern}
value={pattern}
onChange={(e) => updateAllowedParamPattern(index, patternIndex, e.target.value)}
/>
@ -262,7 +263,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
<p className="text-sm font-medium">{t("guardrails.toolPermission.rule.toolType")}</p>
<Input
disabled={disabled}
placeholder="^function$"
placeholder={codeSamples.guardrails.toolTypePattern}
value={rule.tool_type ?? ""}
onChange={(e) =>
updateRule(index, {

View file

@ -24,7 +24,7 @@ export interface PaginatedModelInfoResponse {
size: number;
}
const modelKeys = createQueryKeys("models");
export const modelKeys = createQueryKeys("models");
const modelHubKeys = createQueryKeys("modelHub");
const allProxyModelsKeys = createQueryKeys("allProxyModels");
const selectedTeamModelsKeys = createQueryKeys("selectedTeamModels");

View file

@ -11,6 +11,7 @@ import { PasswordInput } from "@/components/shared/PasswordInput";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { requiredUnlessSiblingSet, tagsControl, textControl } from "./mcpFieldRules";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface IdJagFormFieldsProps {
isEditing?: boolean;
@ -174,7 +175,13 @@ const IdJagFormFields: React.FC<IdJagFormFieldsProps> = ({ isEditing = false })
}
name={["credentials", "client_assertion_signing_alg"]}
>
{(control) => <Input {...textControl(control)} placeholder="RS256" className={fieldClassName} />}
{(control) => (
<Input
{...textControl(control)}
placeholder={codeSamples.mcpServers.idJagSigningAlgorithm}
className={fieldClassName}
/>
)}
</MountedFormField>
<MountedFormField
label={

View file

@ -25,6 +25,8 @@ import type { generalSettingsItem } from "@/app/(dashboard)/router-settings/_com
import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings";
import { toast } from "@/lib/toast";
const DEFAULT_TRANSPORT = "sse";
type MCPStatus = "active" | "pending_review" | "rejected";
const STATUS_CONFIG: Record<MCPStatus, { labelKey: ParseKeys; bg: string; text: string; dot: string }> = {
@ -476,7 +478,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
<div className="flex items-center gap-3 mt-1.5 text-xs text-muted-foreground">
<span>
{t("mcpServers.submissionsTab.card.transport")}{" "}
<span className="text-muted-foreground">{server.transport ?? "sse"}</span>
<span className="text-muted-foreground">{server.transport ?? DEFAULT_TRANSPORT}</span>
</span>
<span>·</span>
<span>

View file

@ -22,6 +22,7 @@ import {
tagsControl,
textControl,
} from "./mcpFieldRules";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface OAuthFlowStatus {
startOAuthFlow: () => void;
@ -373,7 +374,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
{(control) => (
<Textarea
{...textControl(control)}
placeholder={'{\n "organization": "my-org",\n "team.id": "123"\n}'}
placeholder={codeSamples.mcpServers.oauthExtraParamsJson}
rows={4}
className="font-mono text-sm rounded-lg border-border focus:border-info focus:ring-ring"
/>

View file

@ -12,6 +12,7 @@ import { requiredRule } from "@/components/common_components/formRules";
import { PasswordInput } from "@/components/shared/PasswordInput";
import { Input } from "@/components/ui/input";
import { selectControl, selectTriggerControl, tagsControl, textControl } from "./mcpFieldRules";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface TokenExchangeFormFieldsProps {
isEditing?: boolean;
@ -193,7 +194,9 @@ const TokenExchangeFormFields: React.FC<TokenExchangeFormFieldsProps> = ({ isEdi
<MultiSelect
{...tagsControl(control, t)}
placeholder={
isEntraObo ? "api://<app-id>/.default" : t("mcpServers.tokenExchangeFormFields.scopes.placeholder")
isEntraObo
? codeSamples.mcpServers.entraOboScope
: t("mcpServers.tokenExchangeFormFields.scopes.placeholder")
}
className="rounded-lg"
/>

View file

@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { textControl } from "./mcpFieldRules";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const UpstreamTokenHeaderField: React.FC = () => {
const { t } = useTranslation();
@ -24,7 +25,7 @@ const UpstreamTokenHeaderField: React.FC = () => {
{(control) => (
<Input
{...textControl(control)}
placeholder="Authorization"
placeholder={codeSamples.mcpServers.upstreamTokenHeader}
className="rounded-lg border-border focus:border-info focus:ring-ring"
/>
)}

View file

@ -23,6 +23,8 @@ import {
} from "lucide-react";
import { getGatewayBaseUrl, getProxyBaseUrl } from "@/components/networking";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { productNames } from "@/i18n/untranslated/productNames";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface CodeBlockProps {
code: string;
@ -354,7 +356,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
icon={<Code className="text-info" size={16} />}
titleKey="mcpServers.connect.implementationExampleCard.title"
description={t("mcpServers.connect.openaiTab.exampleDescription")}
serverName="Zapier Gmail"
serverName={codeSamples.mcpServers.exampleServerName}
accessGroups={["dev-group"]}
>
<CodeBlock
@ -427,7 +429,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
icon={<Code className="text-purple-600 dark:text-purple-400" size={16} />}
titleKey="mcpServers.connect.cursorTab.configurationCard.title"
description={t("mcpServers.connect.cursorTab.configurationCard.description")}
serverName="Zapier Gmail"
serverName={codeSamples.mcpServers.exampleServerName}
accessGroups={["dev-group"]}
>
<CodeBlock
@ -524,25 +526,25 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
<TabsTrigger value="openai" className="flex-none px-6 py-3">
<span className="flex items-center gap-2 font-medium">
<Code size={18} />
OpenAI API
{productNames.openAIApi}
</span>
</TabsTrigger>
<TabsTrigger value="litellm" className="flex-none px-6 py-3">
<span className="flex items-center gap-2 font-medium">
<Zap size={18} />
LiteLLM Proxy
{productNames.liteLLMProxy}
</span>
</TabsTrigger>
<TabsTrigger value="cursor" className="flex-none px-6 py-3">
<span className="flex items-center gap-2 font-medium">
<Terminal size={18} />
Cursor
{productNames.cursor}
</span>
</TabsTrigger>
<TabsTrigger value="http" className="flex-none px-6 py-3">
<span className="flex items-center gap-2 font-medium">
<Globe size={18} />
Streamable HTTP
{productNames.streamableHttp}
</span>
</TabsTrigger>
</div>

View file

@ -11,6 +11,8 @@ import { DiscoverableMCPServer, DiscoverMCPServersResponse } from "@/components/
import { mcpLogoImg } from "./CreateMCPServer";
import { resolveLogoSrc } from "@/lib/assetPaths";
const ALL_CATEGORIES = "All";
interface MCPDiscoveryProps {
isVisible: boolean;
onClose: () => void;
@ -49,7 +51,7 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORIES);
useEffect(() => {
if (isVisible && accessToken) {
@ -72,13 +74,13 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
useEffect(() => {
if (isVisible) {
setSearchQuery("");
setSelectedCategory("All");
setSelectedCategory(ALL_CATEGORIES);
}
}, [isVisible]);
const filteredServers = useMemo(() => {
let result = servers;
if (selectedCategory !== "All") {
if (selectedCategory !== ALL_CATEGORIES) {
result = result.filter((s) => s.category === selectedCategory);
}
if (searchQuery.trim()) {
@ -125,7 +127,7 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
<div className="max-h-[70vh] overflow-y-auto">
{/* Filter pills */}
<div className="mb-3 flex flex-wrap gap-1.5">
{["All", ...categories].map((cat) => {
{[ALL_CATEGORIES, ...categories].map((cat) => {
const isSelected = selectedCategory === cat;
return (
<Button
@ -134,7 +136,7 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
variant={isSelected ? "default" : "outline"}
onClick={() => setSelectedCategory(cat)}
>
{cat === "All" ? t("mcpServers.discovery.allCategories") : cat}
{cat === ALL_CATEGORIES ? t("mcpServers.discovery.allCategories") : cat}
</Button>
);
})}

View file

@ -84,6 +84,7 @@ import {
textControl,
} from "./mcpFieldRules";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface MCPServerEditProps {
mcpServer: MCPServer;
@ -1095,7 +1096,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
{...textControl(control)}
rows={6}
className="rounded-lg border-border focus:border-info focus:ring-ring font-mono text-sm"
placeholder={`{\n \"KEY\": \"value\"\n}`}
placeholder={codeSamples.mcpServers.envJson}
/>
)}
</MountedFormField>

View file

@ -53,6 +53,8 @@ import { cn } from "@/lib/cva.config";
import UserEnvVarsModal from "./UserEnvVarsModal";
import { listMCPUserEnvVarStatus } from "@/components/networking";
const ALL_FILTER_VALUE = "all";
export type SortKey = "created_desc" | "updated_desc" | "name_asc" | "health";
const SORT_OPTIONS: { value: SortKey; labelKey: ParseKeys }[] = [
@ -628,7 +630,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
<Select
items={teamSelectItems}
value={selectedTeam}
onValueChange={(v: string | null) => handleTeamChange(v ?? "all")}
onValueChange={(v: string | null) => handleTeamChange(v ?? ALL_FILTER_VALUE)}
>
<SelectTrigger className="w-55" aria-label={t("mcpServers.servers.filters.team.label")}>
<SelectValue />
@ -663,7 +665,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
<Select
items={accessGroupSelectItems}
value={selectedMcpAccessGroup}
onValueChange={(v: string | null) => handleMcpAccessGroupChange(v ?? "all")}
onValueChange={(v: string | null) => handleMcpAccessGroupChange(v ?? ALL_FILTER_VALUE)}
>
<SelectTrigger
className="w-55"

View file

@ -15,6 +15,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { useZodForm } from "@/lib/forms/useZodForm";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const memorySchema = z.object({
key: z.string().min(1, { error: () => i18n.t("memory.editModal.errors.keyRequired") }),
@ -131,7 +132,13 @@ export const MemoryEditModal: React.FC<MemoryEditModalProps> = ({ open, mode, in
)}
>
{({ ref, ...field }) => (
<Textarea {...field} ref={ref} rows={4} placeholder='{"tags": ["example"]}' className="font-mono" />
<Textarea
{...field}
ref={ref}
rows={4}
placeholder={codeSamples.memory.metadataJson}
className="font-mono"
/>
)}
</FormField>
</FieldGroup>

View file

@ -29,6 +29,7 @@ import {
const SCALES = ["linear", "log"] as const;
const GRANULARITIES = ["day", "week"] as const;
const METRICS = ["requests", "spend", "tokens"] as const;
const GRANULARITY_LABELS: Record<Granularity, ParseKeys> = {
day: "modelInsights.modelInsightsView.granularity.day",
week: "modelInsights.modelInsightsView.granularity.week",
@ -171,7 +172,7 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string
<div className="flex items-center gap-3">
<Tabs value={metric} onValueChange={(value) => setMetric(value as Metric)}>
<TabsList>
{(["requests", "spend", "tokens"] as const).map((value) => (
{METRICS.map((value) => (
<TabsTrigger key={value} value={value} className="capitalize">
{t(METRIC_LABELS[value])}
</TabsTrigger>

View file

@ -127,7 +127,8 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
<Tooltip>
<TooltipTrigger render={<span className="flex items-center text-info" />}>
<Clock className="mr-1 size-3" />
{(totalLatency / 1000).toFixed(2)}s
{(totalLatency / 1000).toFixed(2)}
{t("playground.chatUi.a2aMetrics.secondsSuffix")}
</TooltipTrigger>
<TooltipContent>{t("playground.chatUi.a2aMetrics.totalLatency")}</TooltipContent>
</Tooltip>

View file

@ -6,6 +6,7 @@ import { toast } from "@/lib/toast";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface SessionManagementProps {
endpointType: string | null;
@ -120,15 +121,7 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
<div className="text-xs">
<div className="mb-1">{t("playground.chatUi.sessionManagement.copyToContinue")}</div>
<div className="bg-gray-800 text-gray-100 p-2 rounded-sm font-mono text-xs whitespace-pre-wrap">
{`curl -X POST "your-proxy-url/v1/responses" \\
-H "Authorization: Bearer your-api-key" \\
-H "Content-Type: application/json" \\
-d '{
"model": "your-model",
"input": [{"role": "user", "content": "your message", "type": "message"}],
"previous_response_id": "${responsesSessionId}",
"stream": true
}'`}
{codeSamples.playground.responsesSessionCurl(responsesSessionId)}
</div>
</div>
</TooltipContent>

View file

@ -30,6 +30,7 @@ import SystemOneForm from "./SystemOneForm";
import OpenAIDecisionsForm from "./OpenAIDecisionsForm";
import { validateSystemOnePayload, validateOpenAIDecisionsPayload, errorCount } from "./lib/validatePayload";
import { useDecisionModels, type ApiKeySource } from "./useDecisionModels";
import { productNames } from "@/i18n/untranslated/productNames";
interface SystemOneUIProps {
accessToken: string | null;
@ -272,7 +273,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
<div className="flex flex-wrap items-center justify-between gap-2 border-b px-3 py-2">
<TabsList aria-label={t("playground.systemOne.systemOneUi.requestEditorView")}>
<TabsTrigger value="form">{t("playground.systemOne.systemOneUi.formTab")}</TabsTrigger>
<TabsTrigger value="curl">cURL</TabsTrigger>
<TabsTrigger value="curl">{productNames.curl}</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<ExamplesMenu onPick={handlePresetPick} />

View file

@ -44,7 +44,7 @@ export const readForm = (raw: string): FormPayload | undefined => {
return formPayloadInInputOrder.safeParse(json.value).data;
};
export const freeName = (taken: readonly string[], prefix: string): string =>
export const freeName = (taken: readonly string[], prefix: "option" | "question"): string =>
Array.from({ length: taken.length + 1 }, (_, index) => `${prefix}_${index + 1}`).find(
(name) => !taken.includes(name),
) ?? `${prefix}_${taken.length + 1}`;

View file

@ -26,6 +26,8 @@ import {
import { COMPLIANCE_FRAMEWORK_LABEL_KEYS, getComplianceDatasetPrompts, getFrameworks } from "@/data/compliancePrompts";
import type { CompliancePrompt } from "@/data/compliancePrompts";
const DEFAULT_VERSION_STATUS = "draft";
const TEST_SOURCE_QUICK = "quick_chat";
const TEST_SOURCE_ALL = "__all__";
@ -961,7 +963,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
padding: "0 4px",
}}
>
x
{t("policies.flowBuilder.testPanel.close")}
</button>
</div>
@ -1198,7 +1200,9 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
}}
>
{complianceResults.map((entry, i) => {
const actual = entry.result?.terminal_action ?? (entry.error ? "error" : "—");
const actual =
entry.result?.terminal_action ??
(entry.error ? t("policies.flowBuilder.testPanel.compliance.errorResult") : "—");
const matchStyle = entry.matched
? { bg: "color-mix(in oklab, var(--color-success) 10%, transparent)", color: "var(--color-success)" }
: {
@ -1409,7 +1413,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
borderRadius: 4,
}}
>
{v.version_status ?? "draft"}
{v.version_status ?? DEFAULT_VERSION_STATUS}
</span>
</div>
</button>

View file

@ -23,6 +23,8 @@ import { userDetailHref } from "@/utils/entityLinks";
import { extractModel, getProviderFromModelHub, ModelGroupInfo } from "./prompt_utils";
const DEFAULT_PROMPT_ENVIRONMENT = "development";
const ENVIRONMENT_TONE: Record<string, StatusTone> = {
production: "error",
staging: "warning",
@ -99,7 +101,7 @@ function PromptRowActions({ prompt, isAdmin, onDeleteClick }: PromptRowActionsPr
onDeleteClick?.(
prompt.prompt_id,
prompt.prompt_id || t("prompts.table.rowActions.unknownPrompt"),
prompt.environment || "development",
prompt.environment || DEFAULT_PROMPT_ENVIRONMENT,
)
}
>
@ -142,7 +144,7 @@ export const getPromptTableColumns = ({
className="max-w-60"
onClick={
onPromptClick
? () => onPromptClick(row.original.prompt_id, row.original.environment || "development")
? () => onPromptClick(row.original.prompt_id, row.original.environment || DEFAULT_PROMPT_ENVIRONMENT)
: undefined
}
/>
@ -184,7 +186,7 @@ export const getPromptTableColumns = ({
size: 130,
enableSorting: false,
cell: ({ row }) => {
const environment = row.original.environment || "development";
const environment = row.original.environment || DEFAULT_PROMPT_ENVIRONMENT;
return <StatusBadge tone={ENVIRONMENT_TONE[environment] ?? "neutral"} label={environment} />;
},
},

View file

@ -15,6 +15,8 @@ import PublishModal from "./PublishModal";
import DotpromptViewTab from "./DotpromptViewTab";
import VersionHistorySidePanel from "./VersionHistorySidePanel";
const versionedPromptId = (promptId: string, version: number) => `${promptId}.v${version}`;
const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess, accessToken, initialPromptData }) => {
const { t } = useTranslation();
const getInitialPrompt = (): PromptType => {
@ -283,7 +285,7 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
const loadedPrompt = parseExistingPrompt(response);
setPrompt({ ...loadedPrompt, environment: env });
const versionNum = response.prompt_spec.version || 1;
setActiveVersionId(`${response.prompt_spec.prompt_id}.v${versionNum}`);
setActiveVersionId(versionedPromptId(response.prompt_spec.prompt_id, versionNum));
}
} catch {
// No version in this environment yet — keep current content, just change env

View file

@ -5,6 +5,7 @@ import { Switch } from "@/components/ui/switch";
import type { ParseKeys } from "i18next";
import { Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
export interface RequestTagMapping {
header: "baggage";
@ -111,7 +112,7 @@ export function RequestTagMappingsEditor({ value, onChange, disabled = false }:
{t("routerSettings.requestTagMappings.baggageKey")}
<Input
aria-label={t("routerSettings.requestTagMappings.baggageKeyAriaLabel", { number: index + 1 })}
placeholder="service"
placeholder={codeSamples.routerSettings.baggageKey}
value={rule.key}
onChange={(event) => update(index, { key: event.target.value })}
/>
@ -145,13 +146,16 @@ export function RequestTagMappingsEditor({ value, onChange, disabled = false }:
{t("routerSettings.requestTagMappings.tagPrefix")}
<Input
aria-label={t("routerSettings.requestTagMappings.tagPrefixAriaLabel", { number: index + 1 })}
placeholder="app:"
placeholder={codeSamples.routerSettings.tagPrefix}
value={rule.tag_prefix ?? ""}
onChange={(event) => update(index, { tag_prefix: event.target.value })}
/>
</label>
<p className="text-xs text-muted-foreground">
{rule.key}=x → {rule.tag_prefix}x
{rule.key}
{codeSamples.routerSettings.anyValuePreviewSeparator}
{rule.tag_prefix}
{codeSamples.routerSettings.anyValuePreviewValue}
</p>
</>
)}

View file

@ -16,6 +16,9 @@ import { isValidSemanticVersion } from "@/components/claude_code_plugins/helpers
import { isPendingSkill, type SkillHubSkill, type SkillMutationResponse } from "@/components/claude_code_plugins/types";
import { buildSkillImport, buildSkillMetadata, skillFormValues } from "@/components/claude_code_plugins/skillForm";
const EDIT_SKILL_SOURCES = ["metadata", "upload"] as const;
const NEW_SKILL_SOURCES = ["upload", "import"] as const;
interface AddPluginFormProps {
visible: boolean;
onClose: () => void;
@ -119,7 +122,7 @@ export default function AddPluginForm({
<FormField control={form.control} name="mode" label={t("skills.addForm.labels.source")}>
{({ value }) => (
<RadioGroup value={value} onValueChange={changeMode} className="flex gap-5">
{(skill ? (["metadata", "upload"] as const) : (["upload", "import"] as const)).map((option) => (
{(skill ? EDIT_SKILL_SOURCES : NEW_SKILL_SOURCES).map((option) => (
<label key={option} className="flex items-center gap-2 text-sm">
<RadioGroupItem value={option} />
{t(`skills.addForm.modes.${option}`)}

View file

@ -109,7 +109,7 @@ export function ProviderSpendBreakdown({ rows }: { rows: readonly ProviderSpendD
<div
key={row.provider}
data-testid="provider-share-segment"
title={`${row.provider || "unknown"} · ${shareOf(row.spend).toFixed(1)}%`}
title={`${row.provider || t("usage.providerSpend.unknownProvider")} · ${shareOf(row.spend).toFixed(1)}%`}
className="h-full first:rounded-l-full last:rounded-r-full"
style={{ width: `${shareOf(row.spend)}%`, backgroundColor: colorOf(index) }}
/>
@ -119,7 +119,7 @@ export function ProviderSpendBreakdown({ rows }: { rows: readonly ProviderSpendD
{visible.slice(0, 6).map((row, index) => (
<span key={row.provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2 rounded-[2px]" style={{ backgroundColor: colorOf(index) }} />
<span className="text-foreground">{row.provider || "unknown"}</span>
<span className="text-foreground">{row.provider || t("usage.providerSpend.unknownProvider")}</span>
<span className="tabular-nums">{shareOf(row.spend).toFixed(1)}%</span>
</span>
))}

View file

@ -107,6 +107,8 @@ import { useTagSummary } from "@/app/(dashboard)/hooks/tags/useTagSummary";
import { Panel } from "./overview/Primitives";
import UsageOverview from "./overview/UsageOverview";
const GATEWAY_ENDPOINT_COLORS = ["#2b3fd6", "#ef4444"];
interface UsagePageProps {
teams: Team[];
organizations: Organization[];
@ -585,7 +587,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations, teamsLoadin
t("usage.pageView.gatewayByEndpoint.successful"),
t("usage.pageView.gatewayByEndpoint.failed"),
]}
colors={["#2b3fd6", "#ef4444"]}
colors={GATEWAY_ENDPOINT_COLORS}
xKey="route"
format={(value) => value.toLocaleString()}
className="h-64"

View file

@ -29,6 +29,8 @@ import {
type DefaultUserSettingsSubmitValues,
} from "./schema";
const DEFAULT_TEAM_ROLE = "user";
const NO_RESET = "never";
const BUDGET_DURATION_OPTIONS: readonly { value: string; labelKey: ParseKeys }[] = [
@ -162,7 +164,11 @@ const TeamsField = ({ control }: { control: SettingsControl }) => {
label={t("users.defaultSettings.teams.role")}
>
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
<Select items={teamRoleItems} value={value} onValueChange={(selected) => onChange(selected ?? "user")}>
<Select
items={teamRoleItems}
value={value}
onValueChange={(selected) => onChange(selected ?? DEFAULT_TEAM_ROLE)}
>
<SelectTrigger
id={id}
className="w-full"

View file

@ -17,6 +17,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableToolbar,
} from "@/components/shared/DataTable";
import { SearchSelect } from "@/components/shared/SearchSelect";
@ -183,7 +184,7 @@ export function UsersTable({
title={t("users.table.filters.title")}
description={t("users.table.filters.description")}
>
{({ get, set }) => (
{({ get, set }: FilterDraft<"sso_user_id" | "team" | "user_id" | "user_role">) => (
<>
<DataTableFilterField label={t("users.table.filters.userId")}>
<Input

View file

@ -38,6 +38,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { useZodForm } from "@/lib/forms/useZodForm";
import { SearchSelect } from "@/components/shared/SearchSelect";
import { credentialOptions } from "@/components/shared/credentialOptions";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const EMBEDDING_MODEL_RENAME_PROVIDERS = new Set(["milvus", "valkey", "mongodb", "opensearch"]);
@ -521,7 +522,7 @@ const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
rows={4}
value={metadataJson}
onChange={(event) => setMetadataJson(event.target.value)}
placeholder='{"key": "value"}'
placeholder={codeSamples.vectorStores.metadataJson}
/>
</div>
</FieldGroup>

View file

@ -30,6 +30,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { useZodForm } from "@/lib/forms/useZodForm";
import { SearchSelect } from "@/components/shared/SearchSelect";
import { credentialOptions } from "@/components/shared/credentialOptions";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const DEFAULT_VECTOR_STORE_PROVIDER = "bedrock";
interface VectorStoreInfoViewProps {
vectorStoreId: string;
@ -353,7 +356,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
rows={4}
value={metadataString}
onChange={(event) => setMetadataString(event.target.value)}
placeholder='{"key": "value"}'
placeholder={codeSamples.vectorStores.metadataJson}
/>
</div>
</FieldGroup>
@ -394,7 +397,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
<p className="font-medium">{t("vectorStores.info.details.provider")}</p>
<div className="flex items-center space-x-2 mt-1">
{(() => {
const provider = vectorStoreDetails.custom_llm_provider || "bedrock";
const provider = vectorStoreDetails.custom_llm_provider || DEFAULT_VECTOR_STORE_PROVIDER;
const { displayName, logo } = getVectorStoreProviderLogoAndName(provider);
return (

View file

@ -8,6 +8,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableToolbar,
} from "@/components/shared/DataTable";
import { Button } from "@/components/ui/button";
@ -560,7 +561,7 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
title={t("workflows.filters.title")}
description={t("workflows.filters.description")}
>
{({ get, set }) => (
{({ get, set }: FilterDraft<"status" | "workflow_type">) => (
<>
<DataTableFilterField label={t("workflows.filters.status")}>
<Select

View file

@ -31,6 +31,7 @@ import { Trans, useTranslation } from "react-i18next";
import { z } from "zod";
import { useWorker } from "@/hooks/useWorker";
import i18n from "@/i18n/i18n";
import { productNames } from "@/i18n/untranslated/productNames";
const loginSchema = z.object({
username: z.string().min(1, { error: () => i18n.t("login.schema.usernameRequired") }),
@ -211,7 +212,7 @@ function LoginPageContent() {
<CardContent>
<div className="flex w-full flex-col gap-4">
<div className="text-center">
<h2 className="text-3xl font-semibold text-foreground">🚅 LiteLLM</h2>
<h2 className="text-3xl font-semibold text-foreground">{productNames.liteLLMWithLogo}</h2>
</div>
<Alert variant="warning">
@ -238,7 +239,7 @@ function LoginPageContent() {
<TooltipProvider>
<div className="flex w-full flex-col gap-4">
<div className="text-center">
<h2 className="text-3xl font-semibold text-foreground">🚅 LiteLLM</h2>
<h2 className="text-3xl font-semibold text-foreground">{productNames.liteLLMWithLogo}</h2>
</div>
<div className="text-center">

View file

@ -4,6 +4,7 @@ import { Suspense, useEffect, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
import { productNames } from "@/i18n/untranslated/productNames";
// Written to sessionStorage so the admin hook (useMcpOAuthFlow), the user hook
// (useUserMcpOAuthFlow), and the tools re-auth hook (useToolsOAuthFlow) can each
@ -81,7 +82,7 @@ const McpOAuthCallbackContent = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-muted p-6">
<div className="max-w-lg w-full rounded-lg bg-card shadow-md p-8 text-center space-y-4">
<h1 className="text-xl font-semibold text-foreground">LiteLLM MCP OAuth</h1>
<h1 className="text-xl font-semibold text-foreground">{productNames.liteLLMMcpOAuth}</h1>
<p className="text-sm text-foreground">{t("mcpOAuthCallback.authorizationComplete")}</p>
<p className="text-xs text-muted-foreground">{t("mcpOAuthCallback.manualCloseHint")}</p>
</div>

View file

@ -13,6 +13,7 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { cn } from "@/lib/cva.config";
import { useZodForm } from "@/lib/forms/useZodForm";
import i18n from "@/i18n/i18n";
import { productNames } from "@/i18n/untranslated/productNames";
type OnboardingFormBodyProps = {
variant: "signup" | "reset_password";
@ -41,7 +42,7 @@ export function OnboardingFormBody({ variant, userEmail, isPending, claimError,
<div className="mx-auto w-full max-w-md mt-10">
<Card>
<CardContent>
<h5 className="text-center mb-5 text-base font-semibold text-foreground">🚅 LiteLLM</h5>
<h5 className="text-center mb-5 text-base font-semibold text-foreground">{productNames.liteLLMWithLogo}</h5>
<h3 className="text-2xl font-semibold text-foreground">{actionLabel}</h3>
<p className="text-sm text-foreground">
{isResetPassword ? t("onboarding.formBody.resetDescription") : t("onboarding.formBody.signUpDescription")}

View file

@ -833,8 +833,6 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
<div className="flex flex-wrap gap-2">
{(() => {
const capabilities = getModelCapabilities(selectedModel);
const colors = ["green", "blue", "purple", "orange", "red", "yellow"];
if (capabilities.length === 0) {
return (
<p className="text-muted-foreground">{t("aiHub.modelHubTable.modelDialog.noCapabilities")}</p>

View file

@ -274,7 +274,12 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
const userIdField = (
<>
<FormField control={form.control} name="supply_user_id" label="Supply custom User ID" orientation="horizontal">
<FormField
control={form.control}
name="supply_user_id"
label={t("users.createUser.fields.supplyUserId")}
orientation="horizontal"
>
{({ id, value, onChange, onBlur }) => (
<Checkbox
id={id}
@ -291,8 +296,8 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
<FormField
control={form.control}
name="user_id"
label="User ID"
description="A unique ID for this user. Leave the checkbox unchecked to generate one automatically."
label={t("users.createUser.fields.userId")}
description={t("users.createUser.fields.userIdDescription")}
>
{({ ref, value, ...control }) => <Input {...control} ref={ref} value={value ?? ""} />}
</FormField>

View file

@ -50,6 +50,7 @@ import {
isPositiveWholePtuCount,
ptuWindowIsOrdered,
} from "../utils/ptuValidation";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface PtuEditField {
name: string;
@ -811,7 +812,7 @@ const ModelInfoEditForm: React.FC<ModelInfoEditFormProps> = ({
<Textarea
{...control}
rows={4}
placeholder={'{"gpt-4": 100, "claude-v1": 200}'}
placeholder={codeSamples.models.perModelLimitsJson}
defaultValue={JSON.stringify(modelData.model_info, null, 2)}
/>
)}
@ -841,7 +842,7 @@ const ModelInfoEditForm: React.FC<ModelInfoEditFormProps> = ({
aria-label={t("modelInfoEditForm.fields.litellmParams.label")}
value={(value as string) ?? ""}
rows={4}
placeholder={'{\n "drop_params": true\n}'}
placeholder={codeSamples.models.litellmParamsJson}
/>
)}
</FormField>

View file

@ -16,6 +16,7 @@ import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"
import { uiHref } from "@/utils/uiHref";
import { cn } from "@/lib/cva.config";
import i18n from "@/i18n/i18n";
import { productNames } from "@/i18n/untranslated/productNames";
const GATEWAY = "ai-gateway";
const CHAT = "chat";
@ -123,7 +124,7 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
label: (
<div className="flex w-full items-center justify-between gap-6 py-0.5">
<span className="flex flex-col">
<span className="font-medium">Moyai</span>{" "}
<span className="font-medium">{productNames.moyai}</span>{" "}
<span className="text-xs leading-snug text-muted-foreground">
{t("shell.viewSwitcher.moyaiDescription")}
</span>

View file

@ -20,6 +20,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import CyberArkEmptyPlaceholder from "./CyberArkEmptyPlaceholder";
import EditCyberArkModal from "./EditCyberArkModal";
import { FIELD_LABELS, SENSITIVE_FIELDS } from "./constants";
import { productNames } from "@/i18n/untranslated/productNames";
function detectAuthMethod(values: Record<string, unknown>, t: TFunction): string {
if (values.cyberark_api_key) return t("adminPanel.cyberArk.authMethods.apiKey");
@ -145,7 +146,7 @@ export default function CyberArk() {
<KeyRound className="size-6 text-muted-foreground" />
<div>
<CardTitle>
<h3>CyberArk Conjur</h3>
<h3>{productNames.cyberArkConjur}</h3>
</CardTitle>
<CardDescription>{t("adminPanel.cyberArk.description")}</CardDescription>
</div>

View file

@ -20,6 +20,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import EditHashicorpVaultModal from "./EditHashicorpVaultModal";
import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder";
import { FIELD_LABELS, SENSITIVE_FIELDS } from "./constants";
import { productNames } from "@/i18n/untranslated/productNames";
function detectAuthMethod(values: Record<string, unknown>, t: TFunction): string {
if (values.approle_role_id || values.approle_secret_id) return "AppRole";
@ -142,7 +143,7 @@ export default function HashicorpVault() {
<KeyRound className="size-6 text-muted-foreground" />
<div>
<CardTitle>
<h3>Hashicorp Vault</h3>
<h3>{productNames.hashicorpVault}</h3>
</CardTitle>
<CardDescription>{t("adminPanel.hashicorpVault.description")}</CardDescription>
</div>

View file

@ -31,6 +31,7 @@ import {
storedValuesToForm,
ToolSearchFormValues,
} from "./toolSearchForm";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface MCPToolSearchSettingsProps {
accessToken: string | null;
@ -232,7 +233,7 @@ export default function MCPToolSearchSettings({ accessToken }: MCPToolSearchSett
id={id}
ref={ref}
value={value}
placeholder={"my_server-get_rates\nmy_server-list_accounts"}
placeholder={codeSamples.mcpServers.toolSearchToolNames}
onChange={(event) => onChange(event.target.value)}
onBlur={onBlur}
disabled={isUpdating}

View file

@ -16,6 +16,7 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { useZodForm } from "@/lib/forms/useZodForm";
import { pluginSchema, type PluginFormValues } from "./schema";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { productNames } from "@/i18n/untranslated/productNames";
const INLINE_CODE_CLASS = "rounded-sm bg-muted px-1 py-0.5 font-mono text-xs";
@ -209,7 +210,7 @@ export default function PluginSettings() {
name="display_name"
label={t("adminPanel.pluginSettings.modal.fields.displayName.label")}
>
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="Agent Control Plane" />}
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder={productNames.agentControlPlane} />}
</FormField>
<FormField
control={form.control}

View file

@ -11,6 +11,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
const pageVisibilityCheckboxId = (page: string) => `page-visibility-${page}`;
interface PageVisibilitySettingsProps {
enabledPagesInternalUsers: string[] | null | undefined;
enabledPagesPropertyDescription?: string;
@ -89,7 +91,7 @@ export default function PageVisibilitySettings({
</legend>
<div className="ml-4 space-y-2">
{pages.map((page) => {
const checkboxId = `page-visibility-${page.page}`;
const checkboxId = pageVisibilityCheckboxId(page.page);
return (
<label key={page.page} htmlFor={checkboxId} className="flex cursor-pointer items-start gap-2">
<Checkbox

View file

@ -18,6 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Label } from "@/components/ui/label";
import PageVisibilitySettings from "./PageVisibilitySettings";
import BackgroundChatSettings from "./BackgroundChatSettings";
import { productNames } from "@/i18n/untranslated/productNames";
interface SettingRowProps {
ariaLabel: string;
@ -453,7 +454,7 @@ export default function UISettings() {
<Separator />
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">Moyai</p>
<p className="text-sm font-medium text-foreground">{productNames.moyai}</p>
<p className="text-sm text-muted-foreground">
{moyaiUrl
? t("adminPanel.uiSettings.moyai.connectedTo", { url: moyaiUrl })

View file

@ -23,6 +23,7 @@ import { ChevronsUpDown, Crown, IdCard, KeyRound, LogOut, Mail, ShieldCheck } fr
import { useRouter } from "next/navigation";
import React from "react";
import { Trans, useTranslation } from "react-i18next";
import { productNames } from "@/i18n/untranslated/productNames";
const RELEASE_NOTES_URL = "https://docs.litellm.ai/release_notes";
@ -193,7 +194,7 @@ const SidebarAccountMenu: React.FC<SidebarAccountMenuProps> = ({ onLogout, colla
data-testid="sidebar-account-menu-panel"
>
<div className="flex items-center gap-2 border-b border-border px-3 py-3">
<span className="text-[15px] font-bold tracking-tight text-foreground">LiteLLM</span>
<span className="text-[15px] font-bold tracking-tight text-foreground">{productNames.liteLLM}</span>
{!disableBouncingIcon && (
<span
className="animate-bounce text-lg leading-none"

View file

@ -72,6 +72,7 @@ import { teamCreateCall } from "./networking";
import { normalizeTeamModelSelection } from "./team/teamModelAccess";
import { ModelSelect } from "./ModelSelect/ModelSelect";
import { StorePromptsToggle } from "./common_components/StorePromptsToggle";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const SUPPRESSED_BY_DESCRIPTION = "";
@ -1010,7 +1011,7 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, isViewOnly
ref={ref}
value={value ?? ""}
rows={4}
placeholder='{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}'
placeholder={codeSamples.teams.vaultConfigJson}
disabled={!premiumUser}
/>
)}

View file

@ -7,6 +7,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableToolbar,
} from "@/components/shared/DataTable";
import { SearchSelect } from "@/components/shared/SearchSelect";
@ -206,7 +207,7 @@ export function TeamsTable({ userRole, userID, onSelectTeam, onEditTeam, onDelet
title={t("teams.table.filters.title")}
description={t("teams.table.filters.description")}
>
{({ get, set }) => (
{({ get, set }: FilterDraft<"org_id" | "team_id" | "alias">) => (
<>
<DataTableFilterField label={t("teams.table.filters.organization")}>
<SearchSelect

View file

@ -7,6 +7,8 @@ import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
const oppositeTheme = (isDark: boolean) => (isDark ? "light" : "dark");
const ThemeToggle: React.FC = () => {
const { t } = useTranslation();
const { setTheme, resolvedTheme } = useTheme();
@ -20,7 +22,7 @@ const ThemeToggle: React.FC = () => {
aria-label={label}
title={label}
className="text-muted-foreground"
onClick={() => setTheme(isDark ? "light" : "dark")}
onClick={() => setTheme(oppositeTheme(isDark))}
>
{isDark ? <Moon /> : <Sun />}
</Button>

View file

@ -16,6 +16,7 @@ import { useZodForm } from "@/lib/forms/useZodForm";
import i18n from "@/i18n/i18n";
import { toast } from "@/lib/toast";
import { getSSOSettings, updateSSOSettings } from "./networking";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface UIAccessControlFormProps {
accessToken: string | null;
@ -211,7 +212,9 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
t("adminPanel.uiAccessControl.fields.jwtFieldHint"),
)}
>
{({ ref, value, ...field }) => <Input {...field} ref={ref} value={value ?? ""} placeholder="groups" />}
{({ ref, value, ...field }) => (
<Input {...field} ref={ref} value={value ?? ""} placeholder={codeSamples.users.ssoGroupsClaim} />
)}
</FormField>
</FieldGroup>

View file

@ -124,7 +124,7 @@ interface KeySummaryStripProps {
const KeySummaryStrip: React.FC<KeySummaryStripProps> = ({ summary, loading, hidePromptCachingMetrics }) => {
const { t } = useTranslation();
const daily = useMemo(() => summarySeries(summary), [summary]);
const trend = (dataKey: string) =>
const trend = (dataKey: "spend" | "requests" | "tokens") =>
loading || daily.length < 2 ? undefined : (
<Sparkline data={daily} dataKey={dataKey} color={BRAND} className="h-12" />
);

View file

@ -11,6 +11,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableToolbar,
usePersistedColumnVisibility,
useUrlTableState,
@ -298,7 +299,7 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
title={t("apiKeys.virtualKeysTable.filters.title")}
description={t("apiKeys.virtualKeysTable.filters.description")}
>
{({ get, set }) => (
{({ get, set }: FilterDraft<"key_hash" | "org_id" | "status" | "team_id" | "user_id">) => (
<>
<DataTableFilterField label={t("apiKeys.virtualKeysTable.filters.team")}>
<SearchSelect

View file

@ -28,6 +28,8 @@ import type { ModelTopKeysResponse } from "./UsagePage/dailyActivityApi";
import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopModelData } from "./UsagePage/types";
import { averageResponseTimeMs, formatResponseTime, valueFormatter } from "./UsagePage/utils/value_formatters";
const UNKNOWN_MODEL_NAME = "Unknown Model";
interface ActivityMetricsProps {
modelMetrics: Record<string, ModelActivityData>;
summaryMetrics?: ModelActivityData;
@ -680,7 +682,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
}
>
<ModelSection
modelName={modelName || "Unknown Model"}
modelName={modelName || UNKNOWN_MODEL_NAME}
metrics={metrics}
hidePromptCachingMetrics={hidePromptCachingMetrics}
fetchTopApiKeys={fetchTopApiKeys}

View file

@ -34,6 +34,7 @@ import {
isAllowanceExhausted,
AUTO_ROUTER_CONTACT_URL,
} from "./AutoRouterAvailability";
import { productNames } from "@/i18n/untranslated/productNames";
export function ClassifierOption({
value,
@ -229,7 +230,13 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({
description: t("addModel.autoRouterClassifierTabs.families.decisionModelDescription"),
},
...(switchyard
? [{ value: "switchyard", label: "Switchyard", description: "Route by the conversation’s tool history." }]
? [
{
value: "switchyard",
label: productNames.switchyard,
description: t("addModel.autoRouterClassifierTabs.families.switchyardDescription"),
},
]
: []),
].map((option) => (
<Label
@ -262,10 +269,10 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({
<Label htmlFor={`${id}-decision-model`}>{t("addModel.autoRouterClassifierTabs.decisionModelLabel")}</Label>
<Select
items={[
{ value: "jev", label: "Jev" },
{ value: "laya", label: "Laya" },
{ value: "bespoke", label: "Bespoke Nimble" },
{ value: "databricks", label: "Databricks" },
{ value: "jev", label: productNames.jev },
{ value: "laya", label: productNames.laya },
{ value: "bespoke", label: productNames.bespokeNimble },
{ value: "databricks", label: productNames.databricks },
]}
value={normalizeJevClassifierConfig(value.jev_classifier_config).provider}
onValueChange={changeProvider}
@ -274,10 +281,10 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="jev">Jev</SelectItem>
<SelectItem value="laya">Laya</SelectItem>
<SelectItem value="bespoke">Bespoke Nimble</SelectItem>
<SelectItem value="databricks">Databricks</SelectItem>
<SelectItem value="jev">{productNames.jev}</SelectItem>
<SelectItem value="laya">{productNames.laya}</SelectItem>
<SelectItem value="bespoke">{productNames.bespokeNimble}</SelectItem>
<SelectItem value="databricks">{productNames.databricks}</SelectItem>
</SelectContent>
</Select>
</div>

View file

@ -5,6 +5,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { ROUTER_FAMILIES, ROUTER_GOALS, type FamilyPreference, type RouterGoal, type SolverModel } from "./auto_setup";
const TIER_ROLES = ["everyday", "hard"] as const;
type Props = {
customized: boolean;
hasSuggestions: boolean;
@ -144,7 +146,7 @@ export default function AutoRouterSimpleSetup({
)}
</div>
<div className="grid gap-3 sm:grid-cols-2">
{(["everyday", "hard"] as const).map((role) => {
{TIER_ROLES.map((role) => {
const chosen = role === "everyday" ? everyday : hard;
const other = role === "everyday" ? hard : everyday;
const choices = models.filter((model) => model.name !== other?.name);

View file

@ -9,6 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Slider } from "@/components/ui/slider";
import type { CustomDimensionRow } from "./custom_dimensions";
const MATCH_FIELDS = ["keywords", "patterns"] as const;
const SCORING_MODES = [
{ value: "binary", labelKey: "addModel.customDimensionRows.scoringModes.binary" },
{ value: "match_count", labelKey: "addModel.customDimensionRows.scoringModes.matchCount" },
@ -87,7 +89,7 @@ export default function CustomDimensionRows({ rows, disabled, onChange, onWeight
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{(["keywords", "patterns"] as const).map((field) => (
{MATCH_FIELDS.map((field) => (
<div key={field} className="min-w-0 space-y-1">
<Label htmlFor={`${row.id}-${field}`}>
{field === "keywords"

View file

@ -32,6 +32,8 @@ import { activeTierRows } from "./tier_rows";
import { setTierModels } from "./tier_set_actions";
import { tierRowLabel, setTierModelParam, setTierModelReasoningEffort } from "./complexity_router_tiers";
const CALIBRATION_ROLES = ["efficient", "capable"] as const;
interface Props {
value: ComplexityRouterConfigValue;
onChange: (value: ComplexityRouterConfigValue) => void;
@ -401,7 +403,7 @@ const ForecastClassifierConfig = ({ value, onChange, modelOptions, effortOptions
)}
{!isCapability &&
fuse.calibration &&
(["efficient", "capable"] as const).map((role) => (
CALIBRATION_ROLES.map((role) => (
<CalibrationFields
key={role}
label={

View file

@ -13,6 +13,7 @@ import {
ClassificationRubric,
ComplexityTierLabels,
} from "./ComplexityRouterConfig";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
export type OpeningPromptTierSource =
| { kind: "custom"; tierRows: readonly TierRow[] }
@ -252,7 +253,7 @@ const OpeningPromptEditor: React.FC<OpeningPromptEditorProps> = ({
value={exampleDraft}
onChange={(e) => setExampleDraft(e.target.value)}
rows={6}
placeholder={'- "what is the capital of France?" -> SIMPLE'}
placeholder={codeSamples.models.openingPromptExample}
aria-label={t("addModel.openingPromptEditor.examples.label")}
className="mt-2 font-mono text-xs"
/>

View file

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
import { getReminderMarkersError, type ReminderMarkerPair } from "./build_complexity_router_config";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const ReminderMarkers: React.FC<{
value: ComplexityRouterConfigValue;
@ -42,7 +43,7 @@ const ReminderMarkers: React.FC<{
</label>
<Input
id={`reminder-marker-${index}-open`}
placeholder="<note>"
placeholder={codeSamples.models.reminderMarkerOpen}
value={marker.open}
onChange={(event) => update(index, { open: event.target.value })}
/>
@ -53,7 +54,7 @@ const ReminderMarkers: React.FC<{
</label>
<Input
id={`reminder-marker-${index}-close`}
placeholder="</note>"
placeholder={codeSamples.models.reminderMarkerClose}
value={marker.close}
onChange={(event) => update(index, { close: event.target.value })}
/>

View file

@ -14,6 +14,8 @@ import {
parseToolSemantics,
type SwitchyardConfig,
} from "./switchyard_config";
import { useTranslation } from "react-i18next";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface Props {
value: SwitchyardConfig;
@ -23,6 +25,15 @@ interface Props {
onToolSemanticsError: (error: string | null) => void;
}
const TIERS = ["capable", "efficient"] as const;
const LLM_NUMBER_FIELDS = [
["base_threshold", "addModel.switchyardConfigFields.llmFields.capabilityThreshold"],
["threshold_step", "addModel.switchyardConfigFields.llmFields.thresholdStep"],
["max_output_tokens", "addModel.switchyardConfigFields.llmFields.maxOutputTokens"],
["recent_turn_window", "addModel.switchyardConfigFields.llmFields.recentTurnWindow"],
] as const;
const isStructured = (instructions: unknown): instructions is Record<string, unknown> =>
typeof instructions === "object" && instructions !== null;
const instructionsText = (instructions: string | Record<string, unknown> | null | undefined): string =>
@ -35,6 +46,7 @@ export default function SwitchyardConfigFields({
decisionModels,
onToolSemanticsError,
}: Props) {
const { t } = useTranslation();
const id = useId();
const [toolSemantics, setToolSemantics] = useState(
value.options.tool_semantics ? JSON.stringify(value.options.tool_semantics, null, 2) : "",
@ -65,24 +77,24 @@ export default function SwitchyardConfigFields({
<div className="grid gap-5 md:grid-cols-2">
<div className="min-w-0 space-y-2">
<span id={`${id}-heuristic`} className="text-sm font-medium">
Heuristic
{t("addModel.switchyardConfigFields.heuristic")}
</span>
<p
aria-labelledby={`${id}-heuristic`}
className="flex min-h-10 items-center rounded-md border bg-muted/30 px-3 py-2 text-sm"
>
Stage Router
{t("addModel.switchyardConfigFields.stageRouter")}
</p>
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-bias`} className="text-sm font-medium">
Bias
{t("addModel.switchyardConfigFields.bias")}
</label>
<Select
value={value.options.picker}
items={[
{ value: "efficient_first", label: "Cost-first" },
{ value: "capable_first", label: "Quality-first (experimental)" },
{ value: "efficient_first", label: t("addModel.switchyardConfigFields.biasOptions.costFirst") },
{ value: "capable_first", label: t("addModel.switchyardConfigFields.biasOptions.qualityFirst") },
]}
onValueChange={(picker) =>
onChange({
@ -98,29 +110,33 @@ export default function SwitchyardConfigFields({
<SelectValue className="whitespace-normal break-words" />
</SelectTrigger>
<SelectContent>
<SelectItem value="efficient_first">Cost-first</SelectItem>
<SelectItem value="efficient_first">
{t("addModel.switchyardConfigFields.biasOptions.costFirst")}
</SelectItem>
<SelectItem value="capable_first">
<span className="whitespace-normal">Quality-first (experimental)</span>
<span className="whitespace-normal">
{t("addModel.switchyardConfigFields.biasOptions.qualityFirst")}
</span>
</SelectItem>
</SelectContent>
</Select>
{value.options.picker === "capable_first" && (
<p className="text-sm text-muted-foreground">
Switchyard has not benchmarked this bias, so it has no calibrated confidence thresholds.
</p>
<p className="text-sm text-muted-foreground">{t("addModel.switchyardConfigFields.qualityFirstWarning")}</p>
)}
</div>
{(["capable", "efficient"] as const).map((tier) => (
{TIERS.map((tier) => (
<div key={tier} className="min-w-0 space-y-2">
<label htmlFor={`${id}-${tier}`} className="text-sm font-medium">
{tier === "capable" ? "Capable model" : "Efficient model"}
{tier === "capable"
? t("addModel.switchyardConfigFields.capableModel")
: t("addModel.switchyardConfigFields.efficientModel")}
</label>
<ModelChoiceCombobox
id={`${id}-${tier}`}
value={value.models[tier]}
onChange={(model) => onChange({ ...value, models: { ...value.models, [tier]: model ?? "" } })}
choices={models}
placeholder="Select a model"
placeholder={t("addModel.switchyardConfigFields.selectModel")}
ariaInvalid={undefined}
ariaDescribedBy={undefined}
/>
@ -131,10 +147,10 @@ export default function SwitchyardConfigFields({
<div className="flex items-center justify-between gap-4">
<div>
<label htmlFor={`${id}-classifier`} className="text-sm font-medium">
Classifier
{t("addModel.switchyardConfigFields.classifier")}
</label>
<p className="text-sm text-muted-foreground">
Ask a judge model when the tool history does not give a confident routing decision.
{t("addModel.switchyardConfigFields.classifierDescription")}
</p>
</div>
<Switch
@ -147,13 +163,13 @@ export default function SwitchyardConfigFields({
<div className="grid gap-5 md:grid-cols-2">
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-classifier-kind`} className="text-sm font-medium">
Classifier type
{t("addModel.switchyardConfigFields.classifierType")}
</label>
<Select
value={classifier.kind}
items={[
{ value: "llm", label: "LLM judge" },
{ value: "decision", label: "Decision model" },
{ value: "llm", label: t("addModel.switchyardConfigFields.classifierKinds.llm") },
{ value: "decision", label: t("addModel.switchyardConfigFields.classifierKinds.decision") },
]}
onValueChange={(kind) => onChange(setClassifierKind(value, kind === "decision" ? "decision" : "llm"))}
>
@ -161,21 +177,29 @@ export default function SwitchyardConfigFields({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="llm">LLM judge</SelectItem>
<SelectItem value="decision">Decision model</SelectItem>
<SelectItem value="llm">{t("addModel.switchyardConfigFields.classifierKinds.llm")}</SelectItem>
<SelectItem value="decision">
{t("addModel.switchyardConfigFields.classifierKinds.decision")}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-judge`} className="text-sm font-medium">
{decision ? "Decision model" : "Judge model"}
{decision
? t("addModel.switchyardConfigFields.decisionModel")
: t("addModel.switchyardConfigFields.judgeModel")}
</label>
<ModelChoiceCombobox
id={`${id}-judge`}
value={value.models.judge ?? ""}
onChange={(judge) => onChange({ ...value, models: { ...value.models, judge: judge ?? "" } })}
choices={decision ? decisionModels : models}
placeholder={decision ? "Select a decision model" : "Select a judge model"}
placeholder={
decision
? t("addModel.switchyardConfigFields.selectDecisionModel")
: t("addModel.switchyardConfigFields.selectJudgeModel")
}
ariaInvalid={undefined}
ariaDescribedBy={undefined}
/>
@ -186,7 +210,7 @@ export default function SwitchyardConfigFields({
<div className="space-y-3">
<div className="flex justify-between">
<span id={`${id}-confidence`} className="text-sm font-medium">
Confidence threshold
{t("addModel.switchyardConfigFields.confidenceThreshold")}
</span>
<span className="text-sm tabular-nums">{stageConfidence(value).toFixed(2)}</span>
</div>
@ -204,39 +228,42 @@ export default function SwitchyardConfigFields({
}
/>
<p className="text-sm text-muted-foreground">
Below this confidence, use {classifier ? "the judge model" : "the bias’s default model"}. Critical errors can
still select the capable model.
{classifier
? t("addModel.switchyardConfigFields.fallbackToJudge")
: t("addModel.switchyardConfigFields.fallbackToDefault")}
</p>
</div>
<details className="rounded-lg border p-4">
<summary className="cursor-pointer text-sm font-medium">Advanced settings</summary>
<summary className="cursor-pointer text-sm font-medium">
{t("addModel.switchyardConfigFields.advancedSettings")}
</summary>
<div className="mt-5 space-y-5">
<h3 className="text-sm font-semibold">Stage</h3>
<h3 className="text-sm font-semibold">{t("addModel.switchyardConfigFields.sections.stage")}</h3>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-recent`}>Recent window</label>
<label htmlFor={`${id}-recent`}>{t("addModel.switchyardConfigFields.recentWindow")}</label>
<Input
id={`${id}-recent`}
type="number"
min={1}
value={value.options.recent_window ?? ""}
placeholder="Library default"
placeholder={t("addModel.switchyardConfigFields.libraryDefault")}
onChange={(event) => setOption("recent_window", event.target.value)}
/>
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-tools`}>Tool semantics</label>
<label htmlFor={`${id}-tools`}>{t("addModel.switchyardConfigFields.toolSemantics")}</label>
<Textarea
id={`${id}-tools`}
value={toolSemantics}
placeholder={'{"observe": ["read_file"], "mutate": ["edit_file"]}'}
placeholder={codeSamples.models.toolSemanticsJson}
onChange={(event) => changeToolSemantics(event.target.value)}
/>
</div>
{decision && (
<>
<h3 className="text-sm font-semibold">Decision model</h3>
<h3 className="text-sm font-semibold">{t("addModel.switchyardConfigFields.sections.decisionModel")}</h3>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-cutoff`}>Advantage cutoff</label>
<label htmlFor={`${id}-cutoff`}>{t("addModel.switchyardConfigFields.advantageCutoff")}</label>
<Input
id={`${id}-cutoff`}
type="number"
@ -249,16 +276,18 @@ export default function SwitchyardConfigFields({
}
/>
<p className="text-sm text-muted-foreground">
Route to the capable model when the decision model’s advantage probability is above this value.
{t("addModel.switchyardConfigFields.advantageCutoffHelp")}
</p>
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-instructions`}>Decision instructions</label>
<label htmlFor={`${id}-instructions`}>
{t("addModel.switchyardConfigFields.decisionInstructions")}
</label>
<Textarea
id={`${id}-instructions`}
readOnly={isStructured(decision.instructions)}
value={instructionsText(decision.instructions)}
placeholder="Use Switchyard’s built-in instructions"
placeholder={t("addModel.switchyardConfigFields.decisionInstructionsPlaceholder")}
onChange={(event) =>
onChange({
...value,
@ -271,18 +300,11 @@ export default function SwitchyardConfigFields({
)}
{llm && (
<>
<h3 className="text-sm font-semibold">Classifier</h3>
<h3 className="text-sm font-semibold">{t("addModel.switchyardConfigFields.sections.classifier")}</h3>
<div className="grid gap-4 md:grid-cols-2">
{(
[
["base_threshold", "Capability threshold"],
["threshold_step", "Threshold step"],
["max_output_tokens", "Maximum output tokens"],
["recent_turn_window", "Recent turn window"],
] as const
).map(([key, label]) => (
{LLM_NUMBER_FIELDS.map(([key, labelKey]) => (
<div key={key} className="min-w-0 space-y-2">
<label htmlFor={`${id}-${key}`}>{label}</label>
<label htmlFor={`${id}-${key}`}>{t(labelKey)}</label>
<Input
id={`${id}-${key}`}
type="number"
@ -296,12 +318,12 @@ export default function SwitchyardConfigFields({
))}
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-format`}>Response format</label>
<label htmlFor={`${id}-format`}>{t("addModel.switchyardConfigFields.responseFormat")}</label>
<Select
value={llm.response_format_type}
items={[
{ value: "json_schema", label: "JSON schema" },
{ value: "json_object", label: "JSON object" },
{ value: "json_schema", label: t("addModel.switchyardConfigFields.responseFormats.jsonSchema") },
{ value: "json_object", label: t("addModel.switchyardConfigFields.responseFormats.jsonObject") },
]}
onValueChange={(format) =>
onChange({
@ -317,17 +339,21 @@ export default function SwitchyardConfigFields({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json_schema">JSON schema</SelectItem>
<SelectItem value="json_object">JSON object</SelectItem>
<SelectItem value="json_schema">
{t("addModel.switchyardConfigFields.responseFormats.jsonSchema")}
</SelectItem>
<SelectItem value="json_object">
{t("addModel.switchyardConfigFields.responseFormats.jsonObject")}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-w-0 space-y-2">
<label htmlFor={`${id}-prompt`}>Classifier prompt</label>
<label htmlFor={`${id}-prompt`}>{t("addModel.switchyardConfigFields.classifierPrompt")}</label>
<Textarea
id={`${id}-prompt`}
value={llm.prompt ?? ""}
placeholder="Use Switchyard’s built-in prompt"
placeholder={t("addModel.switchyardConfigFields.classifierPromptPlaceholder")}
onChange={(event) =>
onChange({ ...value, classifier: { ...llm, prompt: event.target.value || undefined } })
}

View file

@ -16,6 +16,7 @@ import {
} from "./switchyard_config";
import SwitchyardConfigFields from "./SwitchyardConfigFields";
import { handleAddSwitchyardSubmit, type SwitchyardModelInfo } from "./handle_add_auto_router_submit";
import { useTranslation } from "react-i18next";
interface FieldsProps {
accessToken: string;
@ -26,6 +27,7 @@ interface FieldsProps {
}
export function SwitchyardModelFields({ accessToken, routerName, value, onChange, onToolSemanticsError }: FieldsProps) {
const { t } = useTranslation();
const { data: models = [], isError } = useQuery({
queryKey: ["availableModels", "switchyard", accessToken],
queryFn: () => fetchAvailableModels(accessToken),
@ -37,12 +39,10 @@ export function SwitchyardModelFields({ accessToken, routerName, value, onChange
const toChoice = (model: { model_group: string }) => ({ value: model.model_group, label: model.model_group });
return (
<div className="space-y-6">
<p className="text-sm text-muted-foreground">
Route between a capable and an efficient model using the conversation’s tool history.
</p>
<p className="text-sm text-muted-foreground">{t("addModel.switchyardRouterForm.modelFields.description")}</p>
{isError && (
<p role="alert" className="text-sm text-destructive">
Unable to load available models.
{t("addModel.switchyardRouterForm.modelFields.modelsLoadFailed")}
</p>
)}
<SwitchyardConfigFields
@ -110,6 +110,7 @@ interface Props {
}
export default function SwitchyardRouterForm({ accessToken, onSuccess, initial }: Props) {
const { t } = useTranslation();
const id = useId();
const parsed = switchyardConfigSchema.safeParse(initial.config);
const [config, setConfig] = useState<SwitchyardConfig>(parsed.success ? parsed.data : emptySwitchyardConfig());
@ -150,7 +151,7 @@ export default function SwitchyardRouterForm({ accessToken, onSuccess, initial }
>
<div className="space-y-2">
<label htmlFor={`${id}-name`} className="text-sm font-medium">
Router name
{t("addModel.switchyardRouterForm.routerName")}
</label>
<Input id={`${id}-name`} value={name} onChange={(event) => setName(event.target.value)} />
</div>
@ -168,7 +169,7 @@ export default function SwitchyardRouterForm({ accessToken, onSuccess, initial }
)}
<div className="flex justify-end">
<Button type="submit" disabled={saving || Boolean(toolError)}>
{saving ? "Saving…" : "Save changes"}
{saving ? t("addModel.switchyardRouterForm.saving") : t("addModel.switchyardRouterForm.saveChanges")}
</Button>
</div>
</form>

View file

@ -36,6 +36,7 @@ import {
PTU_END_FIELD,
} from "../../utils/ptuValidation";
import { usePtuCostAttributionEnabled } from "@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface AdvancedSettingsProps {
showAdvancedSettings: boolean;
@ -230,7 +231,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
value={(control.value as string | undefined) ?? ""}
onChange={control.onChange}
onBlur={control.onBlur}
placeholder="e.g. 15"
placeholder={t("addModel.advancedSettings.ptuCountPlaceholder")}
/>
)}
</MountedFormField>
@ -257,7 +258,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
value={(control.value as string | undefined) ?? ""}
onChange={control.onChange}
onBlur={control.onBlur}
placeholder="e.g. 2.00"
placeholder={t("addModel.advancedSettings.ptuRatePlaceholder")}
/>
)}
</MountedFormField>
@ -531,11 +532,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
onChange={control.onChange}
onBlur={control.onBlur}
rows={4}
placeholder='{
"rpm": 100,
"timeout": 0,
"stream_timeout": 0
}'
placeholder={codeSamples.models.advancedLitellmParamsJson}
/>
)}
</MountedFormField>
@ -572,9 +569,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
onChange={control.onChange}
onBlur={control.onBlur}
rows={4}
placeholder='{
"mode": "chat"
}'
placeholder={codeSamples.models.advancedModelInfoJson}
/>
)}
</MountedFormField>

View file

@ -122,7 +122,7 @@ const AutoRouterConnectionTest: React.FC<AutoRouterConnectionTestProps> = ({
<span className="font-medium">{target.labels.join(", ")}</span>{" "}
<span className="text-muted-foreground">
{"->"} {target.modelGroup}
{target.mode === "embedding" ? " (embedding)" : ""}
{target.mode === "embedding" ? t("addModel.autoRouterConnectionTest.embeddingSuffix") : ""}
</span>
{result.status === "error" && (
<p className="mt-1 text-xs text-destructive" data-testid="test-error-message">

View file

@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useZodForm } from "@/lib/forms/useZodForm";
import i18n from "@/i18n/i18n";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
const HTTP_METHOD_OPTIONS = HTTP_METHODS.map((method) => ({ label: method, value: method }));
@ -183,7 +184,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
{({ value, onChange, ...field }) => (
<Input
{...field}
placeholder="bria"
placeholder={codeSamples.passThrough.targetPrefix}
value={value ?? ""}
onChange={(event) => {
const raw = event.target.value;

View file

@ -18,6 +18,7 @@ import { CopyToClipboard } from "react-copy-to-clipboard";
import { toast } from "@/lib/toast";
import { downloadBlob } from "@/components/EntityUsageExport/utils";
import { Trans, useTranslation } from "react-i18next";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface BulkCreateUsersProps {
accessToken: string;
@ -590,7 +591,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
<div className="flex items-start">
<div className="w-3 h-3 rounded-full bg-border mt-1.5 mr-2 shrink-0"></div>
<div>
<p className="font-medium">teams</p>
<p className="font-medium">{codeSamples.users.bulkTemplateTeamsColumn}</p>
<p className="text-sm text-muted-foreground">{t("users.bulkCreate.templateColumns.teams")}</p>
</div>
</div>
@ -615,7 +616,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
<div className="flex items-start">
<div className="w-3 h-3 rounded-full bg-border mt-1.5 mr-2 shrink-0"></div>
<div>
<p className="font-medium">models</p>
<p className="font-medium">{codeSamples.users.bulkTemplateModelsColumn}</p>
<p className="text-sm text-muted-foreground">
{t("users.bulkCreate.templateColumns.models")}
</p>

View file

@ -24,6 +24,32 @@ import { ChatAttachment, ChatImageAttachment, ChatMessage } from "./types";
import ChatFileChip from "./ChatFileChip";
import { isChatImageUrl } from "./imageHelpers";
const THINKING_PULSE_CSS = `
@keyframes thinking-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
.chat-thinking-text {
animation: thinking-pulse 1.4s ease-in-out infinite;
}
`;
const TYPING_DOTS_CSS = `
@keyframes chat-typing-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-4px); opacity: 1; }
}
.chat-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--color-muted-foreground);
animation: chat-typing-bounce 1.2s ease-in-out infinite;
}
.chat-dot:nth-child(2) { animation-delay: 0.2s; }
.chat-dot:nth-child(3) { animation-delay: 0.4s; }
`;
const REDACTED_KEY_PATTERNS = /token|key|secret|password|auth/i;
function redactSensitiveValues(obj: Record<string, unknown>): Record<string, unknown> {
@ -442,15 +468,7 @@ function ThinkingPlaceholder() {
const { t } = useTranslation();
return (
<>
<style>{`
@keyframes thinking-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
.chat-thinking-text {
animation: thinking-pulse 1.4s ease-in-out infinite;
}
`}</style>
<style>{THINKING_PULSE_CSS}</style>
<div className="inline-flex items-center gap-1.5 px-2.5 mb-2 bg-muted/50 border rounded-lg text-xs text-muted-foreground">
<span className="chat-thinking-text py-1">{t("chat.messages.thinking")}</span>
</div>
@ -461,21 +479,7 @@ function ThinkingPlaceholder() {
function TypingDots() {
return (
<>
<style>{`
@keyframes chat-typing-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-4px); opacity: 1; }
}
.chat-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--color-muted-foreground);
animation: chat-typing-bounce 1.2s ease-in-out infinite;
}
.chat-dot:nth-child(2) { animation-delay: 0.2s; }
.chat-dot:nth-child(3) { animation-delay: 0.4s; }
`}</style>
<style>{TYPING_DOTS_CSS}</style>
<div className="chat-dot" />
<div className="chat-dot" />
<div className="chat-dot" />

View file

@ -80,7 +80,7 @@ export default function ChatModelPicker({
["all", t("chat.modelPicker.filters.all")],
["chat", t("chat.modelPicker.filters.chat")],
["image", t("chat.modelPicker.filters.images")],
] as const
] satisfies ReadonlyArray<readonly ["all" | ChatMode, string]>
).map(([value, label]) => (
<Button
key={value}

View file

@ -41,6 +41,8 @@ import { useChatShell } from "@/contexts/ChatShellContext";
import ViewSwitcher from "@/components/Navbar/ViewSwitcher";
import ConversationList from "./ConversationList";
const conversationUrl = (chatsRoute: string, id: string) => `${chatsRoute}?id=${encodeURIComponent(id)}`;
export function getChatRoutes() {
const base = uiHref("chat");
return {
@ -193,7 +195,7 @@ const ChatShell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
<ConversationList
conversations={conversations}
activeConversationId={activeConversationId}
onSelect={(id) => navigate(`${routes.chats}?id=${encodeURIComponent(id)}`)}
onSelect={(id) => navigate(conversationUrl(routes.chats, id))}
onDelete={(id) => {
deleteConversation(id);
if (id === activeConversationId) navigate(routes.chats);

View file

@ -5,6 +5,8 @@ import type { MCPEvent } from "@/components/mcp_tools/types";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { cn } from "@/lib/cva.config";
const mcpCallPanelKey = (index: number) => `mcp-call-${index}`;
interface MCPEventsDisplayProps {
events: MCPEvent[];
className?: string;
@ -92,7 +94,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents }: MCPEventsPanelsProps) {
)}
{mcpCallEvents.map((callEvent, index) => {
const key = `mcp-call-${index}`;
const key = mcpCallPanelKey(index);
return (
<MCPEventPanel
key={key}

View file

@ -15,6 +15,7 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { useZodForm } from "@/lib/forms/useZodForm";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import i18n from "@/i18n/i18n";
import { productNames } from "@/i18n/untranslated/productNames";
const cloudZeroSettingsSchema = z.object({
api_key: z.string().min(1, { error: () => i18n.t("usage.cloudzero.validation.apiKeyRequired") }),
@ -219,7 +220,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
<div className="flex items-center gap-2">
<img
src="/cloudzero.png"
alt="CloudZero"
alt={productNames.cloudZero}
className="w-5 h-5"
onError={(e) => {
// Fallback to text if image fails to load

View file

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

View file

@ -9,6 +9,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableSortHeader,
DataTableToolbar,
DEFAULT_PAGE_SIZE_OPTIONS,
@ -317,7 +318,7 @@ export default function MemberTable({
title={t("memberTable.filters.title")}
description={t("memberTable.filters.description")}
>
{({ get, set }) => (
{({ get, set }: FilterDraft<"role">) => (
<DataTableFilterField label={roleTitle}>
<Select
items={roleFilterItems}

View file

@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input";
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table";
import ModelSelector from "./ModelSelector";
import { toast } from "@/lib/toast";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface ModelAliasManagerProps {
accessToken: string;
@ -302,7 +303,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{Object.keys(aliasObject).length === 0 ? (
<span className="text-muted-foreground">
<br />
&nbsp;&nbsp;# No aliases configured yet
{codeSamples.models.noAliasesYamlComment}
</span>
) : (
Object.entries(aliasObject).map(([key, value]) => (

View file

@ -9,6 +9,7 @@ import { Field, FieldLabel } from "@/components/ui/field";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
type GuardrailFields = { request_fields?: string[]; response_fields?: string[] };
type GuardrailSettings = Record<string, GuardrailFields | null>;
@ -56,7 +57,11 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
emit({ ...value, [guardrailName]: isEmpty ? null : updated });
};
const appendField = (guardrailName: string, fieldType: keyof GuardrailFields, field: string) => {
const appendField = (
guardrailName: string,
fieldType: keyof GuardrailFields,
field: "query" | "documents[*]" | "results[*]",
) => {
handleFieldChange(guardrailName, fieldType, [...(value[guardrailName]?.[fieldType] ?? []), field]);
};
@ -139,9 +144,9 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
</div>
<div className="space-y-1 text-xs">
<div>{t("passThroughGuardrailsSection.examples")}</div>
<div>• query</div>
<div>• documents[*].text</div>
<div>• messages[*].content</div>
<div>{codeSamples.passThrough.guardrailRequestQuery}</div>
<div>{codeSamples.passThrough.guardrailRequestDocuments}</div>
<div>{codeSamples.passThrough.guardrailRequestMessages}</div>
</div>
</div>,
)}
@ -154,7 +159,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
disabled={disabled}
onClick={() => appendField(guardrailName, "request_fields", "query")}
>
+ query
{codeSamples.passThrough.guardrailAddQuery}
</Button>
<Button
type="button"
@ -163,7 +168,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
disabled={disabled}
onClick={() => appendField(guardrailName, "request_fields", "documents[*]")}
>
+ documents[*]
{codeSamples.passThrough.guardrailAddDocuments}
</Button>
</div>
</div>
@ -190,8 +195,8 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
</div>
<div className="space-y-1 text-xs">
<div>{t("passThroughGuardrailsSection.examples")}</div>
<div>• results[*].text</div>
<div>• choices[*].message.content</div>
<div>{codeSamples.passThrough.guardrailResponseResults}</div>
<div>{codeSamples.passThrough.guardrailResponseChoices}</div>
</div>
</div>,
)}
@ -204,7 +209,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
disabled={disabled}
onClick={() => appendField(guardrailName, "response_fields", "results[*]")}
>
+ results[*]
{codeSamples.passThrough.guardrailAddResults}
</Button>
</div>
</div>

View file

@ -2,6 +2,7 @@ import React from "react";
import { Trans } from "react-i18next";
import { Badge } from "@/components/ui/badge";
import LoggingSettings from "../team/LoggingSettings";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
interface PremiumLoggingSettingsProps {
value: any[];
@ -23,10 +24,10 @@ export function PremiumLoggingSettings({
<div>
<div className="flex flex-wrap gap-2 mb-3">
<Badge variant="secondary" className="opacity-50">
✨ langfuse-logging
{codeSamples.logging.premiumLangfuse}
</Badge>
<Badge variant="secondary" className="opacity-50">
✨ datadog-logging
{codeSamples.logging.premiumDatadog}
</Badge>
</div>
<div className="p-3 bg-muted border border-border rounded-lg">

View file

@ -826,6 +826,7 @@ const ExistingEditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
};
const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = (props) => {
const { t } = useTranslation();
if (props.modelData?.litellm_params?.model?.startsWith("auto_router/switchyard")) {
return (
<Dialog
@ -836,7 +837,7 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = (props) => {
>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Switchyard router</DialogTitle>
<DialogTitle>{t("editAutoRouter.editAutoRouterModal.switchyardTitle")}</DialogTitle>
</DialogHeader>
<SwitchyardRouterForm
key={props.modelData.model_info.id}

View file

@ -8,6 +8,8 @@ import { toast } from "@/lib/toast";
import { serviceHealthCheck, setCallbacksCall } from "./networking";
import { EmailEventSettings } from "./email_events";
const EMAIL_SERVICE = "email";
interface EmailSettingsProps {
accessToken: string | null;
premiumUser: boolean;
@ -197,7 +199,7 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
onClick={async () => {
if (!accessToken) return;
try {
await serviceHealthCheck(accessToken, "email");
await serviceHealthCheck(accessToken, EMAIL_SERVICE);
toast.success(t("loggingAndAlerts.emailSettings.toasts.testTriggered"));
} catch (error) {
toast.fromError(error);

View file

@ -10,6 +10,7 @@ import {
DataTable,
DataTableFilterDrawer,
DataTableFilterField,
type FilterDraft,
DataTableToolbar,
} from "@/components/shared/DataTable";
import { Input } from "@/components/ui/input";
@ -167,7 +168,10 @@ export function AuditLogsTable({
title={t("logs.audit.table.filtersTitle")}
description={t("logs.audit.table.filtersDescription")}
>
{({ get, set }) => (
{({
get,
set,
}: FilterDraft<"action" | "changed_by" | "key_hash" | "object_id" | "table_name" | "team_id">) => (
<>
<DataTableFilterField label={t("logs.audit.table.objectId")}>
<Input

View file

@ -551,11 +551,13 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
${formatNumberWithCommas(logEntry.spend || 0, 8)}
</DescriptionItem>
<DescriptionItem label={t("logs.detail.metrics.duration")}>
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"}
{t("logs.detail.metrics.secondsSuffix")}
</DescriptionItem>
{ttftMs != null && ttftMs > 0 && (
<DescriptionItem label={t("logs.detail.metrics.timeToFirstToken")}>
{(ttftMs / 1000).toFixed(3)} s
{(ttftMs / 1000).toFixed(3)}
{t("logs.detail.metrics.secondsSuffix")}
</DescriptionItem>
)}

View file

@ -86,7 +86,10 @@ function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) {
<ClassifyTag origin={row.metadata?.internal_call_origin} className="ml-auto" />
</div>
<div className="text-[10px] text-muted-foreground mt-0 flex items-center gap-1.5 font-mono">
<span>{durationValue}s</span>
<span>
{durationValue}
{t("logs.detail.drawer.secondsSuffix")}
</span>
{row.spend ? (
<>
<span>·</span>
@ -363,7 +366,7 @@ export function LogDetailsDrawer({
? mcpCount
: logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
const label = [" LLM", t("logs.detail.drawer.agentCountSuffix"), " MCP"][i];
return count > 0 ? (
<span key={label}>
<span className="mx-1.5">·</span>
@ -377,7 +380,8 @@ export function LogDetailsDrawer({
{isSessionMode && (
<>
<span className="mx-1.5">·</span>
{sessionDurationSeconds}s
{sessionDurationSeconds}
{t("logs.detail.drawer.secondsSuffix")}
</>
)}
</div>

View file

@ -350,7 +350,7 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
}}
>
<Badge variant={response.status === "completed" ? "secondary" : "outline"} style={{ margin: 0 }}>
{response.status || "unknown"}
{response.status || t("logs.detail.realtime.unknownStatus")}
</Badge>
{usage && (
<span className="text-muted-foreground" style={{ fontSize: 11 }}>

View file

@ -258,12 +258,15 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
{response.assessments.map((assess, idx) => {
const policyBadges = (
<div className="flex flex-wrap gap-1">
{assess.wordPolicy && chip("word", "slate")}
{assess.contentPolicy && chip("content", "slate")}
{assess.topicPolicy && chip("topic", "slate")}
{assess.sensitiveInformationPolicy && chip("sensitive-info", "slate")}
{assess.contextualGroundingPolicy && chip("contextual-grounding", "slate")}
{assess.automatedReasoningPolicy && chip("automated-reasoning", "slate")}
{assess.wordPolicy && chip(t("logs.detail.bedrock.policyChips.word"), "slate")}
{assess.contentPolicy && chip(t("logs.detail.bedrock.policyChips.content"), "slate")}
{assess.topicPolicy && chip(t("logs.detail.bedrock.policyChips.topic"), "slate")}
{assess.sensitiveInformationPolicy &&
chip(t("logs.detail.bedrock.policyChips.sensitiveInformation"), "slate")}
{assess.contextualGroundingPolicy &&
chip(t("logs.detail.bedrock.policyChips.contextualGrounding"), "slate")}
{assess.automatedReasoningPolicy &&
chip(t("logs.detail.bedrock.policyChips.automatedReasoning"), "slate")}
</div>
);
@ -415,7 +418,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
>
<div className="flex items-center gap-2">
{chip(r.action ?? t("logs.detail.bedrock.notAvailable"), r.detected ? "red" : "slate")}
<span className="font-medium">{r.name ?? "regex"}</span>
<span className="font-medium">{r.name ?? t("logs.detail.bedrock.unnamedRegex")}</span>
<span className="font-mono text-xs break-all">{r.regex}</span>
</div>
<div className="flex items-center gap-2">
@ -439,7 +442,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
<div key={i} className="px-3 py-1.5 bg-muted rounded-md text-xs">
<div className="flex items-center gap-2">
{chip(t.action ?? i18n.t("logs.detail.bedrock.notAvailable"), t.detected ? "red" : "slate")}
<span className="font-medium">{t.name ?? "topic"}</span>
<span className="font-medium">{t.name ?? i18n.t("logs.detail.bedrock.unnamedTopic")}</span>
{t.type && chip(t.type, "slate")}
{boolPill(t.detected)}
</div>

View file

@ -154,7 +154,9 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
<div key={idx} className="p-3 bg-muted rounded-md">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<KV label={t("logs.detail.contentFilter.pattern")}>{detection.pattern_name || "unknown"}</KV>
<KV label={t("logs.detail.contentFilter.pattern")}>
{detection.pattern_name || t("logs.detail.contentFilter.unknown")}
</KV>
</div>
<div className="space-y-1">
<KV label={t("logs.detail.contentFilter.action")}>
@ -177,7 +179,7 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<KV label={t("logs.detail.contentFilter.keyword")} mono>
{detection.keyword || "unknown"}
{detection.keyword || t("logs.detail.contentFilter.unknown")}
</KV>
{detection.description && (
<KV label={t("logs.detail.contentFilter.description")}>{detection.description}</KV>
@ -207,9 +209,11 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
<div key={idx} className="p-3 bg-muted rounded-md">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<KV label={t("logs.detail.contentFilter.category")}>{detection.category || "unknown"}</KV>
<KV label={t("logs.detail.contentFilter.category")}>
{detection.category || t("logs.detail.contentFilter.unknown")}
</KV>
<KV label={t("logs.detail.contentFilter.keyword")} mono>
{detection.keyword || "unknown"}
{detection.keyword || t("logs.detail.contentFilter.unknown")}
</KV>
{detection.severity && (
<KV label={t("logs.detail.contentFilter.severity")}>

View file

@ -66,9 +66,10 @@ const checkTimeLabel = (value: string, t: TFunction): string => {
};
function HealthStatusBadge({ status }: { status: string }) {
const { t } = useTranslation();
const tone = HEALTH_STATUS_TONES[status];
if (!tone) {
return <StatusBadge tone="neutral" label="unknown" />;
return <StatusBadge tone="neutral" label={t("modelDashboard.healthChecksTableColumns.unknownStatus")} />;
}
return <StatusBadge tone={tone} label={status} />;
}

View file

@ -26,6 +26,7 @@ import {
movePinnedAlias,
orderAliases,
} from "./model_group_alias_ordering";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
type ModelGroupAliasValue = string | { model: string; hidden?: boolean };
@ -542,7 +543,7 @@ const ModelGroupAliasSettings: React.FC<ModelGroupAliasSettingsProps> = ({
{Object.keys(aliasObject).length === 0 ? (
<span className="text-muted-foreground">
<br />
&nbsp;&nbsp;&nbsp;&nbsp;# No aliases configured yet
{codeSamples.models.noModelGroupAliasesYamlComment}
</span>
) : (
Object.entries(aliasObject).map(([key, value]) => (

View file

@ -53,6 +53,7 @@ import { Tag } from "./tag_management/types";
import { getDisplayModelName } from "./view_model/model_name_display";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import KubernetesPodsCard from "./model_info/KubernetesPodsCard";
import { modelKeys } from "@/app/(dashboard)/hooks/models/useModels";
interface ModelInfoViewProps {
modelId: string;
@ -830,7 +831,7 @@ export default function ModelInfoView({
accessToken={accessToken}
modelId={modelId}
onUpdated={() => {
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
queryClient.invalidateQueries({ queryKey: modelKeys.lists() });
}}
/>
)}

View file

@ -21,6 +21,7 @@ import { normalizeMoyaiUrl } from "./moyaiConnect";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import i18n from "@/i18n/i18n";
import styles from "./MoyaiLanding.module.css";
import { productNames } from "@/i18n/untranslated/productNames";
export const MOYAI_GITHUB_URL = "https://github.com/BerriAI/moyai";
export const MOYAI_WALKTHROUGH_URL = `${MOYAI_GITHUB_URL}#see-it-in-action`;
@ -214,7 +215,7 @@ function DemoPreview() {
<span className="size-2.5 rounded-full bg-[#ff5f57]" />
<span className="size-2.5 rounded-full bg-[#febc2e]" />
<span className="size-2.5 rounded-full bg-[#28c840]" />
<span className="ml-3 truncate text-xs text-[#a6a4c6]">github.com/BerriAI/moyai</span>
<span className="ml-3 truncate text-xs text-[#a6a4c6]">{productNames.moyaiRepository}</span>
</div>
{failed ? (
<div className="flex aspect-[1196/720] flex-col items-center justify-center gap-3 text-[#c2c6d0]">
@ -350,7 +351,7 @@ export default function MoyaiLanding({
style={{ animationDelay: "0.08s" }}
>
<img src={moyaiHead.src} alt="" className={`${styles.mark} mb-[0.08em] h-[0.78em] w-auto`} />
<span>Moyai</span>
<span>{productNames.moyai}</span>
</h1>
<p
className={`${styles.rise} mb-0 mt-3 text-[clamp(18px,1.8vw,26px)] tracking-[-0.01em] text-[rgba(226,234,255,0.86)]`}

View file

@ -97,6 +97,23 @@ import { buildKeyCreatePayload, type KeyCreateInput } from "./createKeyPayload";
import { simplifyKeyGenerateError } from "./utils";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const GENERATE_KEY_EXCLUDED_FIELDS = [
"key_alias",
"team_id",
"organization_id",
"models",
"duration",
"metadata",
"tags",
"guardrails",
"max_budget",
"budget_duration",
"tpm_limit",
"rpm_limit",
"tpd_limit",
];
const GENERATE_KEY_EXCLUDED_FIELDS_WITH_KEY = [...GENERATE_KEY_EXCLUDED_FIELDS, "key"];
const KEY_TYPE_OPTIONS: { value: string; label: ParseKeys; hint: ParseKeys }[] = [
{ value: "llm_api", label: "createKey.keyTypes.llmApi.label", hint: "createKey.keyTypes.llmApi.hint" },
{ value: "management", label: "createKey.keyTypes.management.label", hint: "createKey.keyTypes.management.hint" },
@ -1877,22 +1894,11 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
<SchemaFormFields
schemaComponent="GenerateKeyRequest"
setValue={form.setValue}
excludedFields={[
"key_alias",
"team_id",
"organization_id",
"models",
"duration",
"metadata",
"tags",
"guardrails",
"max_budget",
"budget_duration",
"tpm_limit",
"rpm_limit",
"tpd_limit",
...(disableCustomApiKeys ? ["key"] : []),
]}
excludedFields={
disableCustomApiKeys
? GENERATE_KEY_EXCLUDED_FIELDS_WITH_KEY
: GENERATE_KEY_EXCLUDED_FIELDS
}
/>
</CollapsibleContent>
</Collapsible>

View file

@ -20,6 +20,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { useZodForm } from "@/lib/forms/useZodForm";
import i18n from "@/i18n/i18n";
import { codeSamples } from "@/i18n/untranslated/codeSamples";
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
const HTTP_METHOD_OPTIONS = HTTP_METHODS.map((method) => ({ label: method, value: method }));
@ -427,7 +428,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
{...field}
rows={5}
value={value ?? ""}
placeholder='{"Authorization": "Bearer your-token", "Content-Type": "application/json"}'
placeholder={codeSamples.passThrough.headersJson}
/>
)}
</FormField>

Some files were not shown because too many files have changed in this diff Show more