From aa883b580ef2eed0185ba472ddf61a0359e932af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B9=E9=9B=AA?= <2b@yorha.xyz> Date: Thu, 11 Jun 2026 00:11:39 +0900 Subject: [PATCH] feat(ui): i18n for pipeline flow builder --- .../policies/pipeline_flow_builder.tsx | 503 ++++++++++-------- ui/litellm-dashboard/src/locales/en.json | 80 +++ ui/litellm-dashboard/src/locales/zh-CN.json | 80 +++ 3 files changed, 444 insertions(+), 219 deletions(-) diff --git a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx index 70f66018574..b751231bb48 100644 --- a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx +++ b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx @@ -1,4 +1,6 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; import { Select, Typography, Spin } from "antd"; import MessageManager from "@/components/molecules/message_manager"; import { Button, TextInput } from "@tremor/react"; @@ -30,19 +32,19 @@ function getPromptsForTestSource(source: string): CompliancePrompt[] { const { Text } = Typography; -const ACTION_OPTIONS = [ - { label: "Next Step", value: "next" }, - { label: "Allow", value: "allow" }, - { label: "Block", value: "block" }, - { label: "Custom Response", value: "modify_response" }, +const getActionOptions = (t: TFunction) => [ + { label: t("policies.pipelineFlowBuilder.actionNextStep"), value: "next" }, + { label: t("policies.pipelineFlowBuilder.actionAllow"), value: "allow" }, + { label: t("policies.pipelineFlowBuilder.actionBlock"), value: "block" }, + { label: t("policies.pipelineFlowBuilder.actionCustomResponse"), value: "modify_response" }, ]; -const ACTION_LABELS: Record = { - allow: "Allow", - block: "Block", - next: "Next Step", - modify_response: "Custom Response", -}; +const getActionLabels = (t: TFunction): Record => ({ + allow: t("policies.pipelineFlowBuilder.actionAllow"), + block: t("policies.pipelineFlowBuilder.actionBlock"), + next: t("policies.pipelineFlowBuilder.actionNextStep"), + modify_response: t("policies.pipelineFlowBuilder.actionCustomResponse"), +}); function createDefaultStep(): PipelineStep { return { @@ -210,37 +212,40 @@ interface ConnectorProps { onInsert: () => void; } -const Connector: React.FC = ({ onInsert }) => ( -
-
- -
-
-); +const Connector: React.FC = ({ onInsert }) => { + const { t } = useTranslation(); + return ( +
+
+ +
+
+ ); +}; // ───────────────────────────────────────────────────────────────────────────── // Step Card (editable) @@ -263,6 +268,8 @@ const StepCard: React.FC = ({ onDelete, availableGuardrails, }) => { + const { t } = useTranslation(); + const actionOptions = useMemo(() => getActionOptions(t), [t]); const guardrailOptions = availableGuardrails.map((g) => ({ label: g.guardrail_name || g.guardrail_id, value: g.guardrail_name || g.guardrail_id, @@ -292,11 +299,13 @@ const StepCard: React.FC = ({ letterSpacing: "0.06em", }} > - GUARDRAIL + {t("policies.pipelineFlowBuilder.guardrailLabel")}
- Step {stepIndex + 1} + + {t("policies.pipelineFlowBuilder.stepN", { n: stepIndex + 1 })} + @@ -319,12 +328,12 @@ const StepCard: React.FC = ({ {/* Guardrail selector */}
onChange({ on_pass: value as PipelineStep["on_pass"] })} - options={ACTION_OPTIONS} + options={actionOptions} /> {step.on_pass === "modify_response" && (
onChange({ modify_response_message: e.target.value || null })} /> @@ -365,24 +376,26 @@ const StepCard: React.FC = ({
- ON FAIL + + {t("policies.pipelineFlowBuilder.onFail")} +
@@ -409,15 +424,15 @@ const StepCard: React.FC = ({ on_error: value === undefined || value === null ? undefined : (value as PipelineStep["on_error"]), }) } - options={ACTION_OPTIONS} + options={actionOptions} /> {step.on_error === "modify_response" && step.on_fail !== "modify_response" && (
onChange({ modify_response_message: e.target.value || null })} /> @@ -439,6 +454,7 @@ interface PipelineFlowBuilderProps { } const PipelineFlowBuilder: React.FC = ({ pipeline, onChange, availableGuardrails }) => { + const { t } = useTranslation(); const handleInsertStep = (atIndex: number) => { onChange({ ...pipeline, steps: insertStep(pipeline.steps, atIndex) }); }; @@ -481,12 +497,12 @@ const PipelineFlowBuilder: React.FC = ({ pipeline, onC marginBottom: 2, }} > - TRIGGER + {t("policies.pipelineFlowBuilder.triggerLabel")} - Incoming LLM Request + {t("policies.pipelineFlowBuilder.incomingLlmRequest")} - This flow runs when a request matches this policy + {t("policies.pipelineFlowBuilder.triggerDesc")}
@@ -559,10 +575,14 @@ const PipelineFlowBuilder: React.FC = ({ pipeline, onC marginBottom: 2, }} > - END + {t("policies.pipelineFlowBuilder.endLabel")} + + + {t("policies.pipelineFlowBuilder.continueToLlm")} + + + {t("policies.pipelineFlowBuilder.requestProceedsToModel")} - Continue to LLM - Request proceeds to the model
@@ -578,102 +598,116 @@ interface PipelineInfoDisplayProps { pipeline: GuardrailPipeline; } -export const PipelineInfoDisplay: React.FC = ({ pipeline }) => ( -
- {/* Trigger */} -
-
- -
- - TRIGGER - - Incoming LLM Request +export const PipelineInfoDisplay: React.FC = ({ pipeline }) => { + const { t } = useTranslation(); + const actionLabels = useMemo(() => getActionLabels(t), [t]); + return ( +
+ {/* Trigger */} +
+
+ +
+ + {t("policies.pipelineFlowBuilder.triggerLabel")} + + + {t("policies.pipelineFlowBuilder.incomingLlmRequest")} + +
-
- {/* Steps */} - {pipeline.steps.map((step, index) => ( - - {/* Connector */} -
+ {/* Steps */} + {pipeline.steps.map((step, index) => ( + + {/* Connector */} +
- {/* Step card */} -
- {/* Header */} -
-
- - - GUARDRAIL + {/* Step card */} +
+ {/* Header */} +
+
+ + + {t("policies.pipelineFlowBuilder.guardrailLabel")} + +
+ + {t("policies.pipelineFlowBuilder.stepN", { n: index + 1 })} + +
+ + {/* Name */} +
{step.guardrail}
+ + {/* Divider */} +
+ + {/* Pass / Fail / API failure */} +
+ + {" "} + {t("policies.pipelineFlowBuilder.passArrow", { action: actionLabels[step.on_pass] || step.on_pass })} + + + {" "} + {t("policies.pipelineFlowBuilder.onFailArrow", { action: actionLabels[step.on_fail] || step.on_fail })} + + + {" "} + {step.on_error != null + ? t("policies.pipelineFlowBuilder.onApiFailureArrow", { + action: actionLabels[step.on_error] || step.on_error, + }) + : t("policies.pipelineFlowBuilder.onApiFailureArrowSameAsFail", { + action: actionLabels[step.on_fail] || step.on_fail, + })}
- Step {index + 1}
- - {/* Name */} -
{step.guardrail}
- - {/* Divider */} -
- - {/* Pass / Fail / API failure */} -
- - Pass → {ACTION_LABELS[step.on_pass] || step.on_pass} - - - On fail → {ACTION_LABELS[step.on_fail] || step.on_fail} - - - On API failure →{" "} - {step.on_error != null - ? ACTION_LABELS[step.on_error] || step.on_error - : `${ACTION_LABELS[step.on_fail] || step.on_fail} (same as on fail)`} - -
-
- - ))} -
-); + + ))} +
+ ); +}; // ───────────────────────────────────────────────────────────────────────────── // Pipeline Test Panel (right drawer) @@ -685,11 +719,11 @@ interface PipelineTestPanelProps { onClose: () => void; } -const OUTCOME_STYLES: Record = { - pass: { bg: "#f0fdf4", color: "#16a34a", label: "PASS" }, - fail: { bg: "#fef2f2", color: "#dc2626", label: "FAIL" }, - error: { bg: "#fffbeb", color: "#d97706", label: "ERROR" }, -}; +const getOutcomeStyles = (t: TFunction): Record => ({ + pass: { bg: "#f0fdf4", color: "#16a34a", label: t("policies.pipelineFlowBuilder.outcomePass") }, + fail: { bg: "#fef2f2", color: "#dc2626", label: t("policies.pipelineFlowBuilder.outcomeFail") }, + error: { bg: "#fffbeb", color: "#d97706", label: t("policies.pipelineFlowBuilder.outcomeError") }, +}); const TERMINAL_STYLES: Record = { allow: { bg: "#f0fdf4", color: "#16a34a" }, @@ -711,13 +745,17 @@ function complianceMatchExpected(expected: "pass" | "fail", terminalAction: stri return terminalAction === "block"; } -const testSourceOptions = [ - { value: TEST_SOURCE_QUICK, label: "Quick chat (custom message)" }, +const getTestSourceOptions = (t: TFunction) => [ + { value: TEST_SOURCE_QUICK, label: t("policies.pipelineFlowBuilder.testSourceQuickChat") }, ...getFrameworks().map((f) => ({ value: f.name, label: f.name })), - { value: TEST_SOURCE_ALL, label: "All compliance datasets" }, + { value: TEST_SOURCE_ALL, label: t("policies.pipelineFlowBuilder.testSourceAllDatasets") }, ]; const PipelineTestPanel: React.FC = ({ pipeline, accessToken, onClose }) => { + const { t } = useTranslation(); + const testSourceOptions = useMemo(() => getTestSourceOptions(t), [t]); + const outcomeStyles = useMemo(() => getOutcomeStyles(t), [t]); + const actionLabels = useMemo(() => getActionLabels(t), [t]); const [testSource, setTestSource] = useState(TEST_SOURCE_QUICK); const [testMessage, setTestMessage] = useState("Hello, can you help me?"); const [isRunning, setIsRunning] = useState(false); @@ -734,7 +772,7 @@ const PipelineTestPanel: React.FC = ({ pipeline, accessT const emptySteps = pipeline.steps.filter((s) => !s.guardrail); if (emptySteps.length > 0) { - setError("All steps must have a guardrail selected"); + setError(t("policies.pipelineFlowBuilder.allStepsMustHaveGuardrail")); return; } @@ -797,7 +835,9 @@ const PipelineTestPanel: React.FC = ({ pipeline, accessT justifyContent: "space-between", }} > - Test Pipeline + + {t("policies.pipelineFlowBuilder.testPipelineTitle")} +