diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx index c33b5e992fc..0a3b5f9d7e8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ArrowUp, Code2, Square } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from "@/components/ui/input-group"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -38,6 +39,7 @@ export function ChatComposer({ onSuggestionSelect, className, }: ChatComposerProps) { + const { t } = useTranslation(); const submitIfAllowed = () => { if (!submitDisabled && !isLoading) { onSubmit(); @@ -115,7 +117,7 @@ export function ChatComposer({ } > - - {enabled ? "Code Interpreter enabled (click to disable)" : "Enable Code Interpreter"} - + {label} ); } diff --git a/ui/litellm-dashboard/src/components/BetaBadge.tsx b/ui/litellm-dashboard/src/components/BetaBadge.tsx index 4e2195d1c36..69c624751e6 100644 --- a/ui/litellm-dashboard/src/components/BetaBadge.tsx +++ b/ui/litellm-dashboard/src/components/BetaBadge.tsx @@ -1,14 +1,16 @@ +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) { + const { t } = useTranslation(); const disableShowNewBadge = useDisableShowNewBadge(); if (disableShowNewBadge) { return children ? <>{children} : null; } - const badge = dot ? : Beta; + const badge = dot ? : {t("betaBadge.label")}; return children ? ( diff --git a/ui/litellm-dashboard/src/components/UserBanner.tsx b/ui/litellm-dashboard/src/components/UserBanner.tsx index 0c6620133f4..12497e16094 100644 --- a/ui/litellm-dashboard/src/components/UserBanner.tsx +++ b/ui/litellm-dashboard/src/components/UserBanner.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import ReactMarkdown from "react-markdown"; +import { useTranslation } from "react-i18next"; import remarkGfm from "remark-gfm"; import { CircleAlert, Info, TriangleAlert, X } from "lucide-react"; import { Alert, AlertAction, AlertDescription } from "@/components/shared/Alert"; @@ -36,6 +37,7 @@ interface UserBannerProps { } export const UserBanner: React.FC = ({ accessToken }) => { + const { t } = useTranslation(); const { data: banner } = useUserBanner(accessToken); const [dismissedSignature, setDismissedSignature] = useState(() => typeof window === "undefined" ? null : localStorage.getItem(DISMISS_STORAGE_KEY), @@ -62,7 +64,7 @@ export const UserBanner: React.FC = ({ accessToken }) => { - diff --git a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx index 8bdc4005be0..4791d222dce 100644 --- a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx @@ -12,6 +12,7 @@ import { import { z } from "zod"; import { TeamMetadataField } from "@/app/(dashboard)/hooks/teams/useTeamMetadataSchema"; +import i18n from "@/i18n/i18n"; import { FormField } from "@/components/shared/form/FormField"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -24,11 +25,20 @@ export interface MetadataPair { } export const metadataPairsSchema = z - .array(z.object({ key: z.string().min(1, "Missing key"), value: z.string().optional() })) + .array( + z.object({ + key: z.string().min(1, { error: () => i18n.t("metadataKeyValueFields.errors.missingKey") }), + value: z.string().optional(), + }), + ) .superRefine((pairs, ctx) => { pairs.forEach((pair, index) => { if (pair.key && pairs.filter((other) => other.key === pair.key).length > 1) { - ctx.addIssue({ code: "custom", message: "Duplicate key", path: [index, "key"] }); + ctx.addIssue({ + code: "custom", + message: i18n.t("metadataKeyValueFields.errors.duplicateKey"), + path: [index, "key"], + }); } }); }); diff --git a/ui/litellm-dashboard/src/components/liteadmin/AriaAnswer.tsx b/ui/litellm-dashboard/src/components/liteadmin/AriaAnswer.tsx index 2160447b122..8bfd24ea973 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/AriaAnswer.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/AriaAnswer.tsx @@ -1,5 +1,6 @@ import { useRef } from "react"; import { Download } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { ChatMessageContent } from "@/components/chat/ChatMessages"; import type { ChatMessage } from "@/components/chat/types"; import { Button } from "@/components/ui/button"; @@ -18,14 +19,15 @@ export function AriaAnswer({ question?: string; chart?: UsageChart; }) { + const { t } = useTranslation(); const answerRef = useRef(null); const exportAnswer = () => { if (!answerRef.current || !exportAriaAnswer(question ?? "", answerRef.current)) { - toast.error("Could not open the PDF print dialog. Please try again."); + toast.error(t("aria.answer.exportFailed")); } }; return ( -
+
{chart && } diff --git a/ui/litellm-dashboard/src/components/liteadmin/AriaStart.tsx b/ui/litellm-dashboard/src/components/liteadmin/AriaStart.tsx index 4769911ec00..b776b465a9e 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/AriaStart.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/AriaStart.tsx @@ -1,12 +1,26 @@ import type { ReactNode } from "react"; +import type { ParseKeys } from "i18next"; +import { useTranslation } from "react-i18next"; import type { AriaAccess } from "./operations"; -import { ChartNoAxesColumnIncreasing, Coins, TrendingDown } from "lucide-react"; +import { ChartNoAxesColumnIncreasing, Coins, TrendingDown, type LucideIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; -const QUESTIONS = [ - { label: "Spend overview", question: "How much have I spent this week?", Icon: Coins }, - { label: "Cost by model", question: "Which models are costing the most?", Icon: ChartNoAxesColumnIncreasing }, - { label: "Find savings", question: "How can I reduce my spend?", Icon: TrendingDown }, +const QUESTIONS: { label: ParseKeys; question: ParseKeys; Icon: LucideIcon }[] = [ + { + label: "aria.start.questions.spendOverview.label", + question: "aria.start.questions.spendOverview.question", + Icon: Coins, + }, + { + label: "aria.start.questions.costByModel.label", + question: "aria.start.questions.costByModel.question", + Icon: ChartNoAxesColumnIncreasing, + }, + { + label: "aria.start.questions.findSavings.label", + question: "aria.start.questions.findSavings.question", + Icon: TrendingDown, + }, ]; export function AriaStart({ @@ -20,31 +34,34 @@ export function AriaStart({ disabled: boolean; access: AriaAccess; }) { + const { t } = useTranslation(); const usageDescription = - access.mode === "usage" && access.scope.type === "user" ? "Ask about your usage" : "Ask about gateway usage"; - const description = access.mode === "admin" ? "Manage your gateway · Changes require approval" : usageDescription; + access.mode === "usage" && access.scope.type === "user" + ? t("aria.start.descriptionUser") + : t("aria.start.descriptionGateway"); + const description = access.mode === "admin" ? t("aria.start.descriptionAdmin") : usageDescription; return (

- What would you like to know? + {t("aria.start.title")}

{description}

{composer}
-
+
{QUESTIONS.map(({ label, question, Icon }) => ( ))}
diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx index c192241ab1f..3c128b6f6e9 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx @@ -13,6 +13,7 @@ import { } from "react"; import { createPortal } from "react-dom"; import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { uiHref } from "@/utils/uiHref"; @@ -149,19 +150,20 @@ export function AriaSidebarLink({ active }: { active: boolean }) { } export function LiteAdminPage() { + const { t } = useTranslation(); const setPageContainer = useContext(LiteAdminContext)?.setPageContainer; - if (!setPageContainer) - return

Aria is not available for this session.

; + if (!setPageContainer) return

{t("aria.page.unavailable")}

; return
; } export function AriaUsageTrigger() { + const { t } = useTranslation(); const state = useContext(LiteAdminContext); if (!state) return null; return ( - Ask Aria + {t("aria.usageTrigger.ask")} ); } @@ -200,6 +202,7 @@ function Destination({ retry: () => void; open: boolean; }) { + const { t } = useTranslation(); const [approved, setApproved] = useState(false); if (target?.baseUrl && (!target.requiresConsent || approved)) { return ; @@ -208,29 +211,26 @@ function Destination({ - {loading && } + {loading && } {!loading && !target?.baseUrl && ( - {target?.error || "Could not load gateway settings."} + {target?.error || t("aria.destination.settingsError")} )} {!loading && target?.baseUrl && ( - Connect to the configured gateway + {t("aria.destination.consentTitle")}

{target.baseUrl}

-

- This gateway uses a different address. Aria will send your existing session credential to the address - shown above. -

+

{t("aria.destination.consentBody")}

- +
)} @@ -292,6 +292,7 @@ function useForegroundReviewFocus( } function LiteAdminChat({ session, open }: { session: LiteAdminSession; open: boolean }) { + const { t } = useTranslation(); const state = useContext(LiteAdminContext); const chat = useLiteAdmin(session); const [input, setInput] = useState(""); @@ -391,7 +392,7 @@ function LiteAdminChat({ session, open }: { session: LiteAdminSession; open: boo className="text-muted-foreground" > - Start over + {t("aria.chat.startOver")} )} @@ -418,13 +419,14 @@ function LiteAdminComposer({ onModelChange: (value: string | null) => void; tooLong: boolean; }) { + const { t } = useTranslation(); return (
- {tooLong && Keep your message within {MAX_INPUT_LENGTH.toLocaleString()} characters.} + {tooLong && {t("aria.composer.tooLong", { limit: MAX_INPUT_LENGTH.toLocaleString() })}} ; + const { t } = useTranslation(); + if (models.isPending) return ; return ( ({ value: item.model_group, label: item.model_group }))} value={value} onValueChange={onChange} - placeholder="Choose a chat model" - emptyText={models.data?.length ? "No matching models" : "No available models"} + placeholder={t("aria.modelPicker.placeholder")} + emptyText={models.data?.length ? t("aria.modelPicker.noMatch") : t("aria.modelPicker.noModels")} disabled={disabled} allowClear={false} className={compact ? "border-transparent shadow-none dark:bg-transparent" : undefined} @@ -470,13 +473,14 @@ function ModelPicker({ } function ModelStatus({ models }: { models: UseQueryResult }) { + const { t } = useTranslation(); if (!models.isError) return null; return ( - Could not load models.{" "} + {t("aria.modelStatus.loadError")}{" "} diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdminConversation.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdminConversation.tsx index dc92f57a4d3..522aac6a048 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdminConversation.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdminConversation.tsx @@ -1,5 +1,7 @@ import { useEffect, type RefObject } from "react"; +import type { ParseKeys } from "i18next"; import { ArrowDown, Check, ChevronDown } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { MessageScroller, useMessageScroller } from "@shadcn/react/message-scroller"; import CopyButton from "@/components/shared/CopyButton"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -13,12 +15,33 @@ import type { ActionEntry, ConversationEntry } from "./useLiteAdmin"; import { AriaAnswer } from "./AriaAnswer"; import { ARIA_AGENT_LABEL } from "./aria-brand"; -const ACTION_STATUS: Record = { - review: "Review change", - applying: "Applying…", - completed: "Completed", - cancelled: "Cancelled", - unknown: "Check result", +const ACTION_STATUS: Record = { + review: "aria.conversation.status.review", + applying: "aria.conversation.status.applying", + completed: "aria.conversation.status.completed", + cancelled: "aria.conversation.status.cancelled", + unknown: "aria.conversation.status.unknown", +}; + +const ACTION_TITLES: Record = { + key_create: "aria.actions.keyCreate", + key_update: "aria.actions.keyUpdate", + key_detach_from_team: "aria.actions.keyDetachFromTeam", + key_delete: "aria.actions.keyDelete", + key_block: "aria.actions.keyBlock", + key_unblock: "aria.actions.keyUnblock", + team_create: "aria.actions.teamCreate", + team_update: "aria.actions.teamUpdate", + team_delete: "aria.actions.teamDelete", + team_member_add: "aria.actions.teamMemberAdd", + team_member_update: "aria.actions.teamMemberUpdate", + team_member_delete: "aria.actions.teamMemberDelete", + user_create: "aria.actions.userCreate", + user_update: "aria.actions.userUpdate", + user_delete: "aria.actions.userDelete", + budget_create: "aria.actions.budgetCreate", + budget_update: "aria.actions.budgetUpdate", + budget_delete: "aria.actions.budgetDelete", }; interface ConversationProps { @@ -30,12 +53,13 @@ interface ConversationProps { } export function LiteAdminConversation({ entries, thinking, open, reviewRef, onAnswer }: ConversationProps) { + const { t } = useTranslation(); return ( {entries.map((entry) => ( @@ -45,7 +69,7 @@ export function LiteAdminConversation({ entries, thinking, open, reviewRef, onAn ) : (
{entry.message.content} @@ -63,17 +87,21 @@ export function LiteAdminConversation({ entries, thinking, open, reviewRef, onAn ))} {thinking && ( -
+
- Working… + {t("aria.conversation.working")}
)} } > @@ -90,8 +118,12 @@ function ActionCard({ reviewRef, onAnswer, }: Pick & { entry: ActionEntry }) { + const { t } = useTranslation(); const { scrollToEnd } = useMessageScroller(); const review = entry.status === "review"; + const title = Object.hasOwn(ACTION_TITLES, entry.action.name) + ? t(ACTION_TITLES[entry.action.name]) + : entry.action.title; useEffect(() => { if (!open || !review) return; scrollToEnd(); @@ -123,14 +155,14 @@ function ActionCard({ ref={review ? reviewRef : undefined} tabIndex={review ? -1 : undefined} role="region" - aria-label={entry.action.title} + aria-label={title} size="sm" > - {entry.action.title} + {title} {entry.status === "completed" && } - {ACTION_STATUS[entry.status]} + {t(ACTION_STATUS[entry.status])} @@ -140,7 +172,7 @@ function ActionCard({ }> - Details + {t("aria.conversation.details")} {fields} @@ -154,19 +186,19 @@ function ActionCard({
{entry.key} - +
-

Copy this key now. It stays only in this chat session.

+

{t("aria.conversation.keyNotice")}

)}
{review && ( )} diff --git a/ui/litellm-dashboard/src/components/liteadmin/UsageAnswerChart.tsx b/ui/litellm-dashboard/src/components/liteadmin/UsageAnswerChart.tsx index a55cd8165c6..047bf894fed 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/UsageAnswerChart.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/UsageAnswerChart.tsx @@ -1,17 +1,54 @@ +import type { ParseKeys } from "i18next"; +import { useTranslation } from "react-i18next"; import { BarChart } from "@/components/shared/charts/bar_chart"; import { LineChart } from "@/components/shared/charts/line_chart"; import { STACKED_USAGE_PALETTE } from "@/components/shared/charts/StackedUsageChart"; import { formatUsageChartValue, type UsageChart } from "./usageChart"; +const CHART_TITLES: Record = { + "Daily spend": "aria.chart.titles.dailySpend", + "Daily requests": "aria.chart.titles.dailyRequests", + "Daily tokens": "aria.chart.titles.dailyTokens", + "Spend by model": "aria.chart.titles.spendByModel", + "Spend by provider": "aria.chart.titles.spendByProvider", + "Token breakdown": "aria.chart.titles.tokenBreakdown", +}; + +const CHART_SCOPES: Record = { + "Gateway usage": "aria.chart.scopes.gateway", + "All teams": "aria.chart.scopes.allTeams", + "All tags": "aria.chart.scopes.allTags", + "Your usage": "aria.chart.scopes.yours", +}; + +const CHART_LABELS: Record = { + Unknown: "aria.chart.labels.unknown", + Other: "aria.chart.labels.other", + Prompt: "aria.chart.labels.prompt", + Completion: "aria.chart.labels.completion", + "Cache read": "aria.chart.labels.cacheRead", + "Cache creation": "aria.chart.labels.cacheCreation", +}; + +const CHART_NOTES: Record = { + "Up to six leading entries. Other includes remaining recorded spend.": "aria.chart.notes.otherRemainder", + "Cache counts are reported separately. Do not add them to total tokens.": "aria.chart.notes.cacheSeparate", +}; + export function UsageAnswerChart({ chart }: { chart: UsageChart }) { + const { t } = useTranslation(); + const display = (keys: Record, text: string) => (Object.hasOwn(keys, text) ? t(keys[text]) : text); + const title = display(CHART_TITLES, chart.title); const formatValue = (value: number) => formatUsageChartValue(value, chart.unit); - const unavailable = chart.data.filter((point) => point.value == null).map((point) => point.label); - const data = chart.data.map((point) => ({ label: point.label, [chart.unit]: point.value })); + const unavailable = chart.data + .filter((point) => point.value == null) + .map((point) => display(CHART_LABELS, point.label)); + const data = chart.data.map((point) => ({ label: display(CHART_LABELS, point.label), [chart.unit]: point.value })); return ( -
+
-

{chart.title}

+

{title}

{formatUsageChartValue(chart.total, chart.unit)} {chart.unit !== "usd" && ( @@ -20,7 +57,7 @@ export function UsageAnswerChart({ chart }: { chart: UsageChart }) {

- {chart.scope} · {chart.period} + {display(CHART_SCOPES, chart.scope)} · {chart.period}

{chart.kind === "bar" ? ( @@ -51,17 +88,17 @@ export function UsageAnswerChart({ chart }: { chart: UsageChart }) { className="h-40" /> )} - +
- + {chart.data.map((point, index) => ( - + ))} @@ -69,8 +106,8 @@ export function UsageAnswerChart({ chart }: { chart: UsageChart }) {
Category{t("aria.chart.category")} {chart.unit}
{point.label}{display(CHART_LABELS, point.label)} {formatUsageChartValue(point.value, chart.unit)}
{(chart.note || unavailable.length > 0) && (

- {chart.note} - {unavailable.length > 0 && ` Values unavailable for: ${unavailable.join(", ")}.`} + {chart.note && display(CHART_NOTES, chart.note)} + {unavailable.length > 0 && ` ${t("aria.chart.valuesUnavailable", { labels: unavailable.join(", ") })}`}

)}
diff --git a/ui/litellm-dashboard/src/components/liteadmin/agent.ts b/ui/litellm-dashboard/src/components/liteadmin/agent.ts index 4b9b7df3109..b9b8420a6e5 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/agent.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/agent.ts @@ -3,6 +3,7 @@ import type { ChatMessage } from "@/components/chat/types"; import { EndpointType, isModeCompatibleWithEndpoint } from "@/components/chat_ui/mode_endpoint_mapping"; import type { ModelGroup } from "@/components/llm_calls/fetch_models"; import { createGatewayClient } from "@/components/llm_calls/gateway_client"; +import i18n from "@/i18n/i18n"; import { createLiteAdminOperations, type AriaAccess, type OperationContext } from "./operations"; export const MAX_INPUT_LENGTH = 8_000; @@ -53,7 +54,7 @@ export function resolveInferenceTarget(candidate: string, managementBaseUrl: str const invalid: InferenceTarget = { baseUrl: null, requiresConsent: false, - error: "The gateway must be a valid HTTP(S) URL without credentials, a query, or a fragment.", + error: i18n.t("aria.inferenceTarget.invalid"), }; try { const page = new URL(pageUrl); @@ -65,7 +66,7 @@ export function resolveInferenceTarget(candidate: string, managementBaseUrl: str return { baseUrl: null, requiresConsent: false, - error: "Inference must use HTTPS when the dashboard or management gateway uses HTTPS.", + error: i18n.t("aria.inferenceTarget.httpsRequired"), }; } return { baseUrl: target.href, requiresConsent: target.origin !== management.origin, error: null }; @@ -140,10 +141,8 @@ export async function runLiteAdmin(options: LiteAdminOptions, client?: OpenAI): active(); const message = completion.choices[0]?.message; if (message?.tool_calls?.length) { - options.onMessage( - "I reached the step limit. Any completed actions remain applied; check the relevant page before continuing.", - ); + options.onMessage(i18n.t("aria.agent.stepLimit")); } else if (!message?.content && !message?.refusal) { - options.onMessage("The model returned no answer. Try another request or model."); + options.onMessage(i18n.t("aria.agent.noAnswer")); } } diff --git a/ui/litellm-dashboard/src/components/liteadmin/answerExport.ts b/ui/litellm-dashboard/src/components/liteadmin/answerExport.ts index bb3529299df..92fab666a2b 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/answerExport.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/answerExport.ts @@ -1,3 +1,4 @@ +import i18n from "@/i18n/i18n"; import { ARIA_AGENT_LABEL } from "./aria-brand"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; @@ -209,7 +210,7 @@ export function exportAriaAnswer(question: string, answerElement: HTMLElement): const owner = answerElement.ownerDocument; const report = createAriaAnswerDocument(question, answerElement); const frame = owner.createElement("iframe"); - frame.title = `${ARIA_AGENT_LABEL} print preview`; + frame.title = i18n.t("aria.answer.printPreview", { agent: ARIA_AGENT_LABEL }); frame.setAttribute("sandbox", "allow-modals allow-same-origin"); frame.style.cssText = "position:fixed;inset:0;width:0;height:0;border:0;"; owner.body.appendChild(frame); diff --git a/ui/litellm-dashboard/src/components/liteadmin/operations.ts b/ui/litellm-dashboard/src/components/liteadmin/operations.ts index 11b287da07c..bfc8192746f 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/operations.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/operations.ts @@ -1,6 +1,7 @@ import { zodFunction } from "openai/helpers/zod"; import { z } from "zod"; import { apiClient } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import { ApiError } from "@/lib/http/client"; import type { components } from "@/lib/http/schema"; import { generatedKey, projectToolResult, type ResultKind } from "./toolResults"; @@ -192,7 +193,7 @@ export function createLiteAdminOperations(context: OperationContext) { if (action) { const result: ActionResult = { status: "unknown", - message: "The change could not be verified. Check the resource before trying again.", + message: i18n.t("aria.actions.unverified"), }; context.onResult(action, result); throw new Error(result.message); diff --git a/ui/litellm-dashboard/src/components/liteadmin/usageChart.ts b/ui/litellm-dashboard/src/components/liteadmin/usageChart.ts index b6a6d1a008f..82bdd1e3b44 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/usageChart.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/usageChart.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import i18n from "@/i18n/i18n"; import type { AriaAccess } from "./operations"; export const usageChartRequestSchema = z.strictObject({ @@ -141,7 +142,7 @@ export function buildUsageChart(source: UsageChartSource, view: UsageChartView): } export function formatUsageChartValue(value: number | null, unit: UsageChart["unit"]): string { - if (value == null) return "Unavailable"; + if (value == null) return i18n.t("aria.chart.unavailable"); return unit === "usd" ? value.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 6 }) : value.toLocaleString("en-US"); diff --git a/ui/litellm-dashboard/src/components/liteadmin/useLiteAdmin.ts b/ui/litellm-dashboard/src/components/liteadmin/useLiteAdmin.ts index 0bbd62f2241..fdf3cf35a9c 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/useLiteAdmin.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/useLiteAdmin.ts @@ -1,7 +1,9 @@ import { useLayoutEffect, useRef, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; +import type { ParseKeys } from "i18next"; import type { ChatMessage } from "@/components/chat/types"; import { getProxyBaseUrl } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import { extractProxyErrorMessage } from "@/lib/http/client"; import { toast } from "@/lib/toast"; import { getCookie } from "@/utils/cookieUtils"; @@ -32,7 +34,7 @@ type ActiveTurn = { approval?: { id: string; resolve: (approved: boolean) => void }; chart?: UsageChart; }; -const INTERRUPTED_WRITE = "A submitted change may have completed. Check the relevant page before trying again."; +const INTERRUPTED_WRITE: ParseKeys = "aria.session.interruptedWrite"; const RESOURCE_QUERIES = new Set([ "keys", "infiniteKeys", @@ -59,7 +61,7 @@ export function useLiteAdmin(session: LiteAdminSession) { () => () => { const turn = active.current; active.current = null; - if (turn?.outcome === "submitted") toast.warning(INTERRUPTED_WRITE); + if (turn?.outcome === "submitted") toast.warning(i18n.t(INTERRUPTED_WRITE)); turn?.controller.abort(); turn?.approval?.resolve(false); }, @@ -168,10 +170,10 @@ export function useLiteAdmin(session: LiteAdminSession) { } catch (error) { if (active.current !== turn) return; if (!sameSession()) { - if (turn.outcome === "submitted") toast.warning(INTERRUPTED_WRITE); + if (turn.outcome === "submitted") toast.warning(i18n.t(INTERRUPTED_WRITE)); setEntries([]); } else if (!turn.controller.signal.aborted && turn.outcome !== "unknown") { - const context = turn.outcome === "completed" ? "Completed changes are shown above. " : ""; + const context = turn.outcome === "completed" ? `${i18n.t("aria.session.completedChangesShown")} ` : ""; setEntries((current) => [ ...current, { kind: "error", id: crypto.randomUUID(), text: context + extractProxyErrorMessage(error) }, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx b/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx index d33b28e9ba7..74f3f5b9317 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx @@ -181,7 +181,7 @@ const McpCrudPermissionPanel: React.FC = ({ ) : ( )} - {meta.label} + {t(meta.label)} {meta.risk === "high" ? t("mcpTools.crudPermissionPanel.risk.high") @@ -210,7 +210,7 @@ const McpCrudPermissionPanel: React.FC = ({

{/* Checkbox supports `indeterminate`; Switch does not. */} toggleGroup(op, checked)} @@ -223,7 +223,7 @@ const McpCrudPermissionPanel: React.FC = ({ {/* Description row */} {!isCollapsed && (
- {meta.description} + {t(meta.description)}
)} diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json index ea4435d50dc..b237a3444e5 100644 --- a/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/ui/litellm-dashboard/src/i18n/locales/en.json @@ -5553,6 +5553,163 @@ "autoApply": "Changes apply automatically when the proxy refreshes its configuration." } }, + "aria": { + "page": { + "unavailable": "Aria is not available for this session." + }, + "usageTrigger": { + "ask": "Ask Aria" + }, + "destination": { + "loadingSettings": "Loading gateway settings", + "settingsError": "Could not load gateway settings.", + "retry": "Retry", + "consentTitle": "Connect to the configured gateway", + "consentBody": "This gateway uses a different address. Aria will send your existing session credential to the address shown above.", + "useConfigured": "Use configured gateway" + }, + "inferenceTarget": { + "invalid": "The gateway must be a valid HTTP(S) URL without credentials, a query, or a fragment.", + "httpsRequired": "Inference must use HTTPS when the dashboard or management gateway uses HTTPS." + }, + "chat": { + "startOver": "Start over" + }, + "composer": { + "tooLong": "Keep your message within {{limit}} characters.", + "placeholder": "Ask Aria…" + }, + "modelPicker": { + "loading": "Loading models", + "ariaLabel": "{{agent}} model", + "placeholder": "Choose a chat model", + "noMatch": "No matching models", + "noModels": "No available models" + }, + "modelStatus": { + "loadError": "Could not load models.", + "retry": "Retry" + }, + "start": { + "title": "What would you like to know?", + "descriptionAdmin": "Manage your gateway · Changes require approval", + "descriptionUser": "Ask about your usage", + "descriptionGateway": "Ask about gateway usage", + "suggestedQuestions": "Suggested questions", + "questions": { + "spendOverview": { + "label": "Spend overview", + "question": "How much have I spent this week?" + }, + "costByModel": { + "label": "Cost by model", + "question": "Which models are costing the most?" + }, + "findSavings": { + "label": "Find savings", + "question": "How can I reduce my spend?" + } + } + }, + "conversation": { + "ariaLabel": "{{agent}} conversation", + "yourQuestion": "Your question", + "workingLabel": "{{agent}} is working", + "working": "Working…", + "jumpToLatest": "Jump to latest", + "status": { + "review": "Review change", + "applying": "Applying…", + "completed": "Completed", + "cancelled": "Cancelled", + "unknown": "Check result" + }, + "details": "Details", + "copyGeneratedKey": "Copy generated key", + "keyNotice": "Copy this key now. It stays only in this chat session.", + "cancel": "Cancel", + "confirmChange": "Confirm change" + }, + "actions": { + "keyCreate": "Create a virtual key", + "keyUpdate": "Update a virtual key", + "keyDetachFromTeam": "Remove a virtual key from its team without deleting the key", + "keyDelete": "Delete virtual keys", + "keyBlock": "Block a virtual key", + "keyUnblock": "Unblock a virtual key", + "teamCreate": "Create a team", + "teamUpdate": "Update a team", + "teamDelete": "Delete teams", + "teamMemberAdd": "Add a team member", + "teamMemberUpdate": "Update a team member", + "teamMemberDelete": "Remove a team member", + "userCreate": "Create a user without a key", + "userUpdate": "Update a user", + "userDelete": "Delete users", + "budgetCreate": "Create a budget", + "budgetUpdate": "Update a budget", + "budgetDelete": "Delete a budget", + "unverified": "The change could not be verified. Check the resource before trying again." + }, + "session": { + "interruptedWrite": "A submitted change may have completed. Check the relevant page before trying again.", + "completedChangesShown": "Completed changes are shown above." + }, + "agent": { + "stepLimit": "I reached the step limit. Any completed actions remain applied; check the relevant page before continuing.", + "noAnswer": "The model returned no answer. Try another request or model." + }, + "answer": { + "ariaLabel": "{{agent}} answer", + "export": "Export answer", + "exportFailed": "Could not open the PDF print dialog. Please try again.", + "printPreview": "{{agent}} print preview" + }, + "chart": { + "valuesLabel": "{{title}} values", + "category": "Category", + "valuesUnavailable": "Values unavailable for: {{labels}}.", + "unavailable": "Unavailable", + "titles": { + "dailySpend": "Daily spend", + "dailyRequests": "Daily requests", + "dailyTokens": "Daily tokens", + "spendByModel": "Spend by model", + "spendByProvider": "Spend by provider", + "tokenBreakdown": "Token breakdown" + }, + "scopes": { + "gateway": "Gateway usage", + "allTeams": "All teams", + "allTags": "All tags", + "yours": "Your usage" + }, + "labels": { + "unknown": "Unknown", + "other": "Other", + "prompt": "Prompt", + "completion": "Completion", + "cacheRead": "Cache read", + "cacheCreation": "Cache creation" + }, + "notes": { + "otherRemainder": "Up to six leading entries. Other includes remaining recorded spend.", + "cacheSeparate": "Cache counts are reported separately. Do not add them to total tokens." + } + } + }, + "betaBadge": { + "label": "Beta" + }, + "chatComposer": { + "stopRequest": "Stop request", + "sendMessage": "Send message", + "codeInterpreterEnabled": "Code Interpreter enabled (click to disable)", + "enableCodeInterpreter": "Enable Code Interpreter" + }, + "userBanner": { + "dismiss": "Dismiss banner" + }, "caching": { "dashboard": { "tabs": { @@ -6640,6 +6797,10 @@ } }, "metadataKeyValueFields": { + "errors": { + "duplicateKey": "Duplicate key", + "missingKey": "Missing key" + }, "keyPlaceholder": "Key", "valuePlaceholder": "Value", "removePair": "Remove key-value pair", @@ -13523,6 +13684,28 @@ } }, "crudPermissionPanel": { + "groups": { + "read": { + "label": "Read", + "description": "Safe operations — fetch, list, search. No side effects." + }, + "create": { + "label": "Create", + "description": "Add new resources — insert, upload, register." + }, + "update": { + "label": "Update", + "description": "Modify existing resources — edit, patch, rename." + }, + "delete": { + "label": "Delete", + "description": "Destructive operations — remove, purge, destroy." + }, + "unknown": { + "label": "Other", + "description": "Operations that could not be automatically classified." + } + }, "risk": { "high": "High Risk", "medium": "Medium Risk", diff --git a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts index 523995cce46..f7b0c91b140 100644 --- a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts +++ b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts @@ -1,3 +1,5 @@ +import type { ParseKeys } from "i18next"; + export type CrudOp = "read" | "create" | "update" | "delete" | "unknown"; const DELETE_RE = /\b(delete|remove|destroy|purge|drop|erase|unlink)\b/i; @@ -56,31 +58,31 @@ export function groupToolsByCrud(tools: MCPToolEntry[]): Record = { read: { - label: "Read", - description: "Safe operations — fetch, list, search. No side effects.", + label: "mcpTools.crudPermissionPanel.groups.read.label", + description: "mcpTools.crudPermissionPanel.groups.read.description", risk: "low", }, create: { - label: "Create", - description: "Add new resources — insert, upload, register.", + label: "mcpTools.crudPermissionPanel.groups.create.label", + description: "mcpTools.crudPermissionPanel.groups.create.description", risk: "medium", }, update: { - label: "Update", - description: "Modify existing resources — edit, patch, rename.", + label: "mcpTools.crudPermissionPanel.groups.update.label", + description: "mcpTools.crudPermissionPanel.groups.update.description", risk: "medium", }, delete: { - label: "Delete", - description: "Destructive operations — remove, purge, destroy.", + label: "mcpTools.crudPermissionPanel.groups.delete.label", + description: "mcpTools.crudPermissionPanel.groups.delete.description", risk: "high", }, unknown: { - label: "Other", - description: "Operations that could not be automatically classified.", + label: "mcpTools.crudPermissionPanel.groups.unknown.label", + description: "mcpTools.crudPermissionPanel.groups.unknown.description", risk: "unknown", }, };