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 (