From 6085538b33020f8a4aab6801b79297fb7003f9d5 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Nov 2026 20:59:01 -0800 Subject: [PATCH] refactor(ui): move remaining outlier text onto i18n (#50312) * refactor(ui): move remaining outlier text onto i18n Moves 146 English messages that the earlier i18n batches missed into en.json. They live in helpers, zod schemas, label fallbacks and shortcut descriptions that the batch scope script did not count, so no batch picked them up. The English on screen is unchanged. * refactor(ui): keep exported compliance error text in English The compliance results' "Error: ..." text is also written to the CSV export, so it stays a literal. The policy table's unnamed fallback now reads i18n.t inside its helper instead of taking t as a new parameter. --- .../_components/access-group-create/schema.ts | 4 +- .../budgets/_components/BudgetFormFields.tsx | 2 +- .../_components/guardrail_info_helpers.tsx | 2 +- .../_components/ModelInsightsView.tsx | 2 +- .../components/systemOneUI/lib/schemas.ts | 79 +++--- .../policies/_components/PolicyTable.tsx | 8 +- .../ProjectModals/projectFormSchema.ts | 14 +- .../EntityUsageExportModal.tsx | 14 +- .../NotificationsBell/NotificationsBell.tsx | 16 +- .../components/chat_ui/ResponseMetrics.tsx | 16 +- .../ModelAliasManager.test.tsx | 2 +- .../common_components/ModelAliasManager.tsx | 2 +- .../editAutoRouterFormSchema.ts | 18 +- .../email_events/email_event_settings.tsx | 2 +- .../src/components/email_settings.tsx | 8 +- .../src/components/liteadmin/answerExport.ts | 4 +- .../audit/AuditLogDrawer/AuditLogDrawer.tsx | 5 +- .../logs/detail/prettyMessagesUtils.ts | 7 +- .../BedrockGuardrailDetails.tsx | 14 +- .../logs/detail/useKeyboardNavigation.ts | 9 +- .../src/components/page_utils.ts | 6 +- .../src/components/shared/Inspector.tsx | 3 +- .../shared/charts/chart_tooltip.tsx | 4 +- .../src/data/compliancePrompts.ts | 88 +++++++ ui/litellm-dashboard/src/i18n/locales/en.json | 241 ++++++++++++++++++ .../src/utils/ptuValidation.ts | 14 +- 26 files changed, 489 insertions(+), 95 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/schema.ts b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/schema.ts index a0deb4487a0..da6f0985bcb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/schema.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/schema.ts @@ -1,7 +1,9 @@ import { z } from "zod"; +import i18n from "@/i18n/i18n"; + export const accessGroupCreateSchema = z.object({ - name: z.string().refine((value) => value.trim() !== "", "Please enter the access group name"), + name: z.string().refine((value) => value.trim() !== "", { error: () => i18n.t("accessGroups.form.name.required") }), description: z.string(), modelIds: z.array(z.string()), mcpServerIds: z.array(z.string()), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetFormFields.tsx index d5dc12518c2..67d3f45ec95 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetFormFields.tsx @@ -22,7 +22,7 @@ import i18n from "@/i18n/i18n"; import { useZodForm } from "@/lib/forms/useZodForm"; const budgetShape = { - budget_id: z.string().min(1, "Enter a budget ID"), + budget_id: z.string().min(1, { error: () => i18n.t("budgets.form.errors.budgetIdRequired") }), tpm_limit: z.number().nullish(), rpm_limit: z.number().nullish(), tpd_limit: z.number().nullish(), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx index c22e1e5a90d..d0253875735 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx @@ -234,7 +234,7 @@ export const formatGuardrailMode = (raw: unknown): string => { const { tags, default: fallback } = raw as { tags?: Record; default?: unknown }; const tagged: string[] = tags && typeof tags === "object" ? Object.values(tags).flatMap(toModeArray) : []; const modes: string[] = Array.from(new Set([...toModeArray(fallback), ...tagged])); - return modes.length > 0 ? `${modes.join(", ")} (tag-based)` : ""; + return modes.length > 0 ? i18n.t("guardrails.modeDisplay.tagBased", { modes: modes.join(", ") }) : ""; }; // Resolves the supported modes for the selected provider, falling back to the global list diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx index 1e1f3944267..14ac8f1bf8a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx @@ -173,7 +173,7 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string {(["requests", "spend", "tokens"] as const).map((value) => ( - {value} + {t(METRIC_LABELS[value])} ))} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts index 93134b1ee57..52453ac04df 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts @@ -10,10 +10,13 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [ z.looseObject({ type: z.literal("choice"), instructions: decisionInstructions, - criteria: z.record(z.string(), decisionsJson.nullable()).refine((value) => { - const count = Object.keys(value).length; - return count >= 1 && count <= 255; - }, "Choice criteria must contain between 1 and 255 options."), + criteria: z.record(z.string(), decisionsJson.nullable()).refine( + (value) => { + const count = Object.keys(value).length; + return count >= 1 && count <= 255; + }, + { error: () => i18n.t("playground.systemOne.schemas.choiceOptionCount", { max: 255 }) }, + ), }), z .looseObject({ @@ -22,7 +25,7 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [ criteria: z.partialRecord(z.enum(["true", "false"]), decisionsJson.nullable()).nullish(), }) .refine((value) => value.instructions != null || value.criteria != null, { - message: "A yes / no question requires instructions or criteria.", + error: () => i18n.t("playground.systemOne.schemas.noulNeedsInstructionsOrCriteria"), }), z.looseObject({ type: z.literal("score"), @@ -34,13 +37,16 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [ export const decisionsRequestSchema = z.looseObject({ model: z .string() - .refine((value) => value.trim().length > 0, "Model must be a non-empty string.") + .refine((value) => value.trim().length > 0, { error: () => i18n.t("playground.systemOne.schemas.modelNonEmpty") }) .optional(), state: decisionsJson, - questions: z.record(z.string().min(1), decisionQuestionSchema).refine((value) => { - const count = Object.keys(value).length; - return count >= 1 && count <= 128; - }, "Questions must contain between 1 and 128 entries."), + questions: z.record(z.string().min(1), decisionQuestionSchema).refine( + (value) => { + const count = Object.keys(value).length; + return count >= 1 && count <= 128; + }, + { error: () => i18n.t("playground.systemOne.schemas.questionCount") }, + ), }); export type DecisionRequest = z.infer; @@ -49,25 +55,32 @@ export type PlaygroundQuestion = SystemOneQuestion | z.infer - z.string({ error: message }).refine((value) => value.trim().length > 0, message); +const nonEmptyString = (messageKey: ParseKeys) => + z + .string({ error: () => i18n.t(messageKey) }) + .refine((value) => value.trim().length > 0, { error: () => i18n.t(messageKey) }); const instructions = z.string({ error: (iss) => - iss.input === undefined ? "Required property 'instructions' is missing." : "Instructions must be a string.", + iss.input === undefined + ? i18n.t("playground.systemOne.schemas.instructionsMissing") + : i18n.t("playground.systemOne.schemas.instructionsType"), }); const choiceQuestionSchema = z.looseObject({ type: z.literal("choice"), instructions, criteria: z - .record(z.string(), z.string({ error: "Choice descriptions must be strings." }), { - error: "Choice criteria must be an object mapping labels to descriptions.", + .record(z.string(), z.string({ error: () => i18n.t("playground.systemOne.schemas.choiceDescriptionType") }), { + error: () => i18n.t("playground.systemOne.schemas.choiceCriteriaType"), }) - .refine((criteria) => { - const count = Object.keys(criteria).length; - return count >= 1 && count <= MAX_CHOICE_OPTIONS; - }, `Choice criteria must contain between 1 and ${MAX_CHOICE_OPTIONS} options.`), + .refine( + (criteria) => { + const count = Object.keys(criteria).length; + return count >= 1 && count <= MAX_CHOICE_OPTIONS; + }, + { error: () => i18n.t("playground.systemOne.schemas.choiceOptionCount", { max: MAX_CHOICE_OPTIONS }) }, + ), }); const noulQuestionSchema = z.looseObject({ @@ -76,10 +89,10 @@ const noulQuestionSchema = z.looseObject({ criteria: z .looseObject( { - true: z.string({ error: "Noul true criteria must be a string." }).optional(), - false: z.string({ error: "Noul false criteria must be a string." }).optional(), + true: z.string({ error: () => i18n.t("playground.systemOne.schemas.noulTrueType") }).optional(), + false: z.string({ error: () => i18n.t("playground.systemOne.schemas.noulFalseType") }).optional(), }, - { error: "Noul criteria, when provided, must be an object." }, + { error: () => i18n.t("playground.systemOne.schemas.noulCriteriaType") }, ) .optional(), }); @@ -88,10 +101,10 @@ const scoreQuestionSchema = z.looseObject({ type: z.literal("score"), instructions, criteria: z - .array(z.string({ error: "Score levels must be strings." }), { - error: "Score criteria must be an array of levels.", + .array(z.string({ error: () => i18n.t("playground.systemOne.schemas.scoreLevelType") }), { + error: () => i18n.t("playground.systemOne.schemas.scoreCriteriaType"), }) - .min(2, "Score criteria must contain at least 2 levels."), + .min(2, { error: () => i18n.t("playground.systemOne.schemas.scoreLevelCount") }), }); const QUESTION_ERROR_MESSAGES: Partial> = { @@ -108,18 +121,20 @@ const questionSchema = z.discriminatedUnion("type", [choiceQuestionSchema, noulQ export const systemOneRequestSchema = z.looseObject( { - model: nonEmptyString("Model must be a non-empty string.").optional(), - state: z.unknown().nonoptional({ error: "Required property 'state' is missing." }), + model: nonEmptyString("playground.systemOne.schemas.modelNonEmpty").optional(), + state: z.unknown().nonoptional({ error: () => i18n.t("playground.systemOne.schemas.stateMissing") }), questions: z .record(z.string(), questionSchema, { error: (iss) => iss.input === undefined - ? "Required property 'questions' is missing." - : "Questions must be a non-empty object.", + ? i18n.t("playground.systemOne.schemas.questionsMissing") + : i18n.t("playground.systemOne.schemas.questionsType"), }) - .refine((questions) => Object.keys(questions).length > 0, "At least one question is required."), + .refine((questions) => Object.keys(questions).length > 0, { + error: () => i18n.t("playground.systemOne.schemas.questionRequired"), + }), }, - { error: "Payload must be a JSON object." }, + { error: () => i18n.t("playground.systemOne.schemas.payloadType") }, ); const probability = z.number().finite().min(0).max(1); @@ -147,7 +162,7 @@ const scoreAnswerSchema = z.looseObject(scoreAnswerShape); const tokenCount = z.number().finite().nonnegative(); export const systemOneResponseSchema = z.looseObject({ - model: nonEmptyString("Model must be a non-empty string.").nullish(), + model: nonEmptyString("playground.systemOne.schemas.modelNonEmpty").nullish(), answers: z.record( z.string(), z.discriminatedUnion("type", [noulAnswerSchema, choiceAnswerSchema, scoreAnswerSchema]), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/PolicyTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/PolicyTable.tsx index f21b6c85de0..793f9a9cc54 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/PolicyTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/PolicyTable.tsx @@ -6,16 +6,18 @@ import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { DataTable } from "@/components/shared/DataTable"; +import i18n from "@/i18n/i18n"; import { Policy } from "@/components/policies/types"; import { getPolicyTableColumns, PolicyRow } from "./PolicyTableColumns"; /** One row per DB policy name plus one row per config policy, so a config policy never hides same-named DB versions; primaryPolicy is used for display and for Edit (FlowBuilder loads all versions) */ function groupPoliciesByName(policies: Policy[]): PolicyRow[] { + const unnamed = i18n.t("policies.policyTable.unnamed"); const dbPolicies = policies.filter((policy) => policy.definition_location !== "config"); - const names = Array.from(new Set(dbPolicies.map((policy) => policy.policy_name || "(unnamed)"))); + const names = Array.from(new Set(dbPolicies.map((policy) => policy.policy_name || unnamed))); const dbRows = names.map((policyName) => { - const versions = dbPolicies.filter((policy) => (policy.policy_name || "(unnamed)") === policyName); + const versions = dbPolicies.filter((policy) => (policy.policy_name || unnamed) === policyName); const primary = versions.find((version) => version.version_status === "production") ?? [...versions].sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0))[0]; @@ -23,7 +25,7 @@ function groupPoliciesByName(policies: Policy[]): PolicyRow[] { }); const configRows = policies .filter((policy) => policy.definition_location === "config") - .map((policy) => ({ policy_name: policy.policy_name || "(unnamed)", primaryPolicy: policy, versionCount: 1 })); + .map((policy) => ({ policy_name: policy.policy_name || unnamed, primaryPolicy: policy, versionCount: 1 })); return [...dbRows, ...configRows]; } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectModals/projectFormSchema.ts b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectModals/projectFormSchema.ts index 108617e6eb8..bc4c073b724 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectModals/projectFormSchema.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectModals/projectFormSchema.ts @@ -8,7 +8,7 @@ const repeatsEarlierValue = (values: readonly string[], index: number): boolean values[index] !== "" && values.indexOf(values[index]) !== index; const modelLimitSchema = z.object({ - model: z.string().min(1, "Missing model"), + model: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingModel") }), tpm: z.number().optional(), rpm: z.number().optional(), itpm: z.number().optional(), @@ -17,11 +17,15 @@ const modelLimitSchema = z.object({ export const projectFormSchema = z .object({ - project_alias: z.string().min(1, "Please enter a project name"), + project_alias: z.string().min(1, { error: () => i18n.t("projects.form.errors.projectNameRequired") }), team_id: z .string() .nullable() - .pipe(z.string({ error: "Please select a team" }).min(1, "Please select a team")), + .pipe( + z + .string({ error: () => i18n.t("projects.form.errors.teamRequired") }) + .min(1, { error: () => i18n.t("projects.form.errors.teamRequired") }), + ), description: z.string().optional(), models: z.array(z.string()), max_budget: z.number().nullish(), @@ -31,8 +35,8 @@ export const projectFormSchema = z metadata: z .array( z.object({ - key: z.string().min(1, "Missing key"), - value: z.string().min(1, "Missing value"), + key: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingKey") }), + value: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingValue") }), }), ) .optional(), diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx index 16e4c0b34ad..0ae3526693b 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx @@ -1,3 +1,4 @@ +import type { ParseKeys } from "i18next"; import { Loader2 } from "lucide-react"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -7,9 +8,18 @@ import { toast } from "@/lib/toast"; import ExportFormatSelector from "./ExportFormatSelector"; import ExportSummary from "./ExportSummary"; import ExportTypeSelector from "./ExportTypeSelector"; -import type { EntityUsageExportModalProps, ExportFormat, ExportType } from "./types"; +import type { EntityType, EntityUsageExportModalProps, ExportFormat, ExportType } from "./types"; import { downloadBlob, exportFilename } from "./utils"; +const ENTITY_LABEL_KEYS: Record = { + user: "usage.export.modal.entities.user", + team: "usage.export.modal.entities.team", + tag: "usage.export.modal.entities.tag", + organization: "usage.export.modal.entities.organization", + customer: "usage.export.modal.entities.customer", + agent: "usage.export.modal.entities.agent", +}; + const EntityUsageExportModal: React.FC = ({ isOpen, onClose, @@ -24,7 +34,7 @@ const EntityUsageExportModal: React.FC = ({ const [exportType, setExportType] = useState("daily"); const [isExporting, setIsExporting] = useState(false); - const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1); + const entityLabel = t(ENTITY_LABEL_KEYS[entityType]); const modalTitle = customTitle || t("usage.export.modal.title", { entity: entityLabel }); const handleExport = async () => { diff --git a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx index 2d3e8a5c8f2..05e3881373e 100644 --- a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx @@ -131,7 +131,7 @@ export const NotificationsBellView: React.FC = ({ ? t("shell.notifications.bell.triggerUnread", { unread: unreadCount }) : t("shell.notifications.bell.trigger"); if (openDeprecations > 0) { - label = `Notifications, ${openDeprecations} model deprecation ${openDeprecations === 1 ? "alert needs" : "alerts need"} attention`; + label = t("shell.notifications.bell.triggerDeprecations", { count: openDeprecations }); } return ( @@ -166,12 +166,14 @@ export const NotificationsBellView: React.FC = ({ <>
-

Model migrations across your keys and teams.

+

+ {t("shell.notifications.bell.deprecationsDescription")} +

{openAlerts.slice(0, 3).map((alert) => ( @@ -180,12 +182,14 @@ export const NotificationsBellView: React.FC = ({ ))} -

Announcements

+

+ {t("shell.notifications.bell.announcementsHeading")} +

)} {deprecationAlerts && openAlerts.length === 0 && ( - View model deprecations + {t("shell.notifications.bell.viewDeprecations")} )} diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx index f8cf9350309..8cd27c8e352 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx @@ -15,9 +15,6 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage"; -const RESPONSE_CACHE_TOOLTIP = - "This response was replayed from LiteLLM's response cache. The request never reached the provider, so it did not read from or write to the provider's own prompt cache."; - export interface TokenUsage { completionTokens?: number; promptTokens?: number; @@ -58,12 +55,13 @@ function MetricItem({ label, tooltip, icon, value }: MetricItemProps) { } function ResponseCacheIndicator() { + const { t } = useTranslation(); return (