diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts index 47716fccfeb..f65c56e747e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts @@ -3,7 +3,7 @@ import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; import { checkTokenValidity, decodeToken } from "@/utils/jwtUtils"; import { useEffect, useMemo } from "react"; -import { effectiveSessionRole, formatUserRole, isViewOnlySessionRole } from "@/utils/roles"; +import { effectiveSessionRole, formatUserRole, isViewOnlySessionRole, userRoleDisplayLabel } from "@/utils/roles"; import { useCurrentUser } from "./currentUser/useCurrentUser"; import { redirectToLogin } from "./redirectToLogin"; import { useUIConfig } from "./uiConfig/useUIConfig"; @@ -44,7 +44,7 @@ const useAuthorized = () => { userId: currentUser?.user_id ?? null, userEmail: currentUser?.user_email ?? null, userRole: effectiveSessionRole(serverRole), - userRoleLabel: formatUserRole(serverRole), + userRoleLabel: userRoleDisplayLabel(formatUserRole(serverRole)), isViewOnly: isViewOnlySessionRole(serverRole), premiumUser: decoded?.premium_user ?? null, disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index 14a6e3d941f..c4790abe8da 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -758,7 +758,7 @@ const CreateMCPServer: React.FC = ({ > {(control) => ( (control)} items={AUTH_TYPE_ITEMS}> + ({ ...item, label: t(item.label) }))} value={(control.value as string | undefined) ?? null} onValueChange={handleTransportSelected(control.onChange)} > @@ -1014,14 +1014,17 @@ const MCPServerEdit: React.FC = ({ }} > {(control) => ( - (control)} + items={AUTH_TYPE_ITEMS.map((item) => ({ ...item, label: t(item.label) }))} + > {AUTH_TYPE_ITEMS.map((item) => ( - {item.label} + {t(item.label)} ))} diff --git a/ui/litellm-dashboard/src/components/AIHub/MCPHubTableColumns.tsx b/ui/litellm-dashboard/src/components/AIHub/MCPHubTableColumns.tsx index 2807a651648..adb41a11286 100644 --- a/ui/litellm-dashboard/src/components/AIHub/MCPHubTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/MCPHubTableColumns.tsx @@ -158,7 +158,7 @@ export const getMCPHubTableColumns = ({ onServerClick }: MCPHubTableColumnsDeps) ), }, diff --git a/ui/litellm-dashboard/src/components/CommandPalette/CommandPalette.tsx b/ui/litellm-dashboard/src/components/CommandPalette/CommandPalette.tsx index 9323cce85af..1300a9dd71d 100644 --- a/ui/litellm-dashboard/src/components/CommandPalette/CommandPalette.tsx +++ b/ui/litellm-dashboard/src/components/CommandPalette/CommandPalette.tsx @@ -140,7 +140,8 @@ function CommandPaletteContent({ setOpen }: { setOpen: (open: boolean) => void } const pagesGroups = pages.length > 0 ? [{ heading: t("shell.commandPalette.groups.pages"), entries: pages }] : []; if (scope === "global") { - const showSearchKeysAction = !trimmedQuery || "search virtual keys".includes(trimmedQuery.toLowerCase()); + const showSearchKeysAction = + !trimmedQuery || t("shell.commandPalette.searchKeys.title").toLowerCase().includes(trimmedQuery.toLowerCase()); const searchKeysAction: PaletteEntry = { id: "action:search-keys", kind: "search-keys", diff --git a/ui/litellm-dashboard/src/components/DeploymentHealth/DeploymentWarningContent.tsx b/ui/litellm-dashboard/src/components/DeploymentHealth/DeploymentWarningContent.tsx index da2f1b680dc..63eba69b84e 100644 --- a/ui/litellm-dashboard/src/components/DeploymentHealth/DeploymentWarningContent.tsx +++ b/ui/litellm-dashboard/src/components/DeploymentHealth/DeploymentWarningContent.tsx @@ -1,3 +1,4 @@ +import type { ParseKeys } from "i18next"; import type { ReactNode } from "react"; import { Trans } from "react-i18next"; import i18n from "@/i18n/i18n"; @@ -12,9 +13,9 @@ export interface DeploymentWarningContent { docsUrl?: string; } -const STATIC_CONTENT: Readonly> = { +const STATIC_CONTENT: Readonly> = { env_credential_login: { - title: "Environment-credential login is enabled", + title: "shell.deploymentWarnings.envCredentialLogin.title", fix: ( > = { docsUrl: "https://docs.litellm.ai/docs/proxy/ui", }, no_redis: { - title: "No Redis configured", + title: "shell.deploymentWarnings.noRedis.title", fix: , docsUrl: "https://docs.litellm.ai/docs/proxy/redis_requirements", }, detailed_debug: { - title: "Detailed debug logging is on", + title: "shell.deploymentWarnings.detailedDebug.title", fix: ( ({ ...link, label: t(link.label) })), + ...(hideCommunity ? [] : COMMUNITY), + ]; return ( <> {!hideBlog && ( diff --git a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx index 2756af0e60b..2d3e8a5c8f2 100644 --- a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx @@ -37,13 +37,13 @@ interface Announcement { href: string; } -const AUTO_ROUTER_ANNOUNCEMENT: Announcement = { +const autoRouterAnnouncement = (): Announcement => ({ readKey: "litellmHideAutoRouterAnnouncement", - title: "LiteLLM Auto Router", - description: "Route every request to the cheapest model that can handle it, no prompt changes needed.", - linkLabel: "Read the docs", + title: i18n.t("shell.notifications.autoRouter.title"), + description: i18n.t("shell.notifications.autoRouter.description"), + linkLabel: i18n.t("shell.notifications.autoRouter.link"), href: AUTO_ROUTER_DOCS_URL, -}; +}); const describeRelease = ({ new_features, bug_fixes, other_updates }: LatestReleaseInfo): string => [ @@ -121,7 +121,7 @@ export const NotificationsBellView: React.FC = ({ }) => { const { t } = useTranslation(); const upgrade = upgradeAnnouncement(currentVersion, latestRelease); - const announcements = upgrade ? [upgrade, AUTO_ROUTER_ANNOUNCEMENT] : [AUTO_ROUTER_ANNOUNCEMENT]; + const announcements = upgrade ? [upgrade, autoRouterAnnouncement()] : [autoRouterAnnouncement()]; const unreadCount = useUnreadAnnouncementCount(announcements.map((announcement) => announcement.readKey)); const openAlerts = (deprecationAlerts ?? []).filter((alert) => effectiveAlertStatus(alert) === AlertStatus.Open); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index 6fe86c0f352..f7abf42c279 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -332,7 +332,7 @@ export default function UISettings() { changeUnit(Number(event.target.value))} diff --git a/ui/litellm-dashboard/src/components/shared/InheritedBudgetHint.tsx b/ui/litellm-dashboard/src/components/shared/InheritedBudgetHint.tsx index 7168099107d..ed31a867308 100644 --- a/ui/litellm-dashboard/src/components/shared/InheritedBudgetHint.tsx +++ b/ui/litellm-dashboard/src/components/shared/InheritedBudgetHint.tsx @@ -1,5 +1,6 @@ "use client"; +import type { ParseKeys, TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { SimpleTooltip } from "@/components/ui/tooltip"; @@ -141,8 +142,15 @@ export const keyOwnerBudgetSource = ( applyUserBudgetToTeamKeys: boolean, ): UserBudgetSource | null => (!key.team_id || applyUserBudgetToTeamKeys ? key.user ?? null : null); -const formatGate = (gate: InheritedBudgetGate): string => - `${gate.scope} ${gate.alias}: $${formatNumberWithCommas(gate.maxBudget, 2)}${gate.budgetDuration ? ` / ${gate.budgetDuration}` : ""}`; +const SCOPE_LABELS: Record = { + Team: "shared.inheritedBudgetHint.scopes.team", + "Team member": "shared.inheritedBudgetHint.scopes.teamMember", + Organization: "shared.inheritedBudgetHint.scopes.organization", + User: "shared.inheritedBudgetHint.scopes.user", +}; + +const formatGate = (gate: InheritedBudgetGate, t: TFunction): string => + `${t(SCOPE_LABELS[gate.scope])} ${gate.alias}: $${formatNumberWithCommas(gate.maxBudget, 2)}${gate.budgetDuration ? ` / ${gate.budgetDuration}` : ""}`; interface InheritedBudgetHintProps { gates: readonly InheritedBudgetGate[]; @@ -157,7 +165,7 @@ export function InheritedBudgetHint({ gates }: InheritedBudgetHintProps) {
{t("shared.inheritedBudgetHint.intro")} {gates.map((gate) => ( - {formatGate(gate)} + {formatGate(gate, t)} ))}
} diff --git a/ui/litellm-dashboard/src/components/shared/Inspector.tsx b/ui/litellm-dashboard/src/components/shared/Inspector.tsx index ad698ddd37c..eb051bf9d8b 100644 --- a/ui/litellm-dashboard/src/components/shared/Inspector.tsx +++ b/ui/litellm-dashboard/src/components/shared/Inspector.tsx @@ -15,6 +15,7 @@ import { useRef, useState, } from "react"; +import { useTranslation } from "react-i18next"; import { useLocalStorage, useMediaQuery, useOnClickOutside, useWindowSize } from "usehooks-ts"; import { ShortcutHints } from "@/components/shared/ShortcutHints"; @@ -185,6 +186,7 @@ function useExitPresence(item: T | null, itemKey: (item: T) => string) { } function ResizeHandle({ noun, width, onResize }: { noun: string; width: number; onResize: (width: number) => void }) { + const { t } = useTranslation(); const [dragging, setDragging] = useState(false); const onPointerDown = (event: PointerEvent) => { @@ -207,7 +209,7 @@ function ResizeHandle({ noun, width, onResize }: { noun: string; width: number;
- + - = total - 1} onClick={() => step(1)}> + = total - 1} + onClick={() => step(1)} + > - step(-1)}> + step(-1)}> {index >= 0 && ( @@ -274,12 +281,12 @@ function Header() { )}
setFullScreen(!fullScreen)} > {fullScreen ? : } - +
diff --git a/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx b/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx index d59c447186c..7ae66ffb374 100644 --- a/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx +++ b/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx @@ -8,6 +8,7 @@ import { ChevronRight } from "lucide-react"; import { createContext, Fragment, useContext, useState, type ComponentProps, type ReactNode } from "react"; import { Inspector } from "@/components/shared/Inspector"; +import i18n from "@/i18n/i18n"; import { Skeleton } from "@/components/ui/skeleton"; import { TableBody, TableCell, TableHead, TableHeader } from "@/components/ui/table"; import { cn } from "@/lib/cva.config"; @@ -176,7 +177,12 @@ interface IndentProps { readonly className?: string; } -function Indent({ row, toggleLabel = (expanded) => (expanded ? "Collapse" : "Expand"), className }: IndentProps) { +function Indent({ + row, + toggleLabel = (expanded) => + expanded ? i18n.t("shared.inspectorTable.collapse") : i18n.t("shared.inspectorTable.expand"), + className, +}: IndentProps) { if (row.depth > 0) { const last = row.getParentRow()?.subRows.at(-1)?.id === row.id; return ( diff --git a/ui/litellm-dashboard/src/components/shared/ShortcutHints.tsx b/ui/litellm-dashboard/src/components/shared/ShortcutHints.tsx index 934ba5aa9e0..20a9bda21ff 100644 --- a/ui/litellm-dashboard/src/components/shared/ShortcutHints.tsx +++ b/ui/litellm-dashboard/src/components/shared/ShortcutHints.tsx @@ -2,6 +2,7 @@ import { Fragment } from "react"; import { useHotkeysContext } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import { Kbd } from "@/components/ui/kbd"; import { cn } from "@/lib/cva.config"; @@ -62,12 +63,13 @@ export function shortcutHints(shortcuts: readonly BoundShortcut[]): readonly Sho /** The shortcuts active right now, read from what is bound rather than typed by hand. */ export function ShortcutHints({ className }: { className?: string }) { + const { t } = useTranslation(); const { hotkeys } = useHotkeysContext(); const hints = shortcutHints(hotkeys); if (hints.length === 0) return null; return (
diff --git a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx index a97018c3e78..2a0f83fc262 100644 --- a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx +++ b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx @@ -312,7 +312,7 @@ const AdvancedDatePicker: React.FC = ({ onClick={() => handleRelativeTimeSelect(option)} > - {option.label} + {t(option.label)} credential.display_name || credential.credential_name; @@ -16,6 +21,6 @@ export const toCredentialOption = (credential: CredentialItem): SearchSelectOpti }); export const credentialOptions = (credentials: CredentialItem[]): SearchSelectOption[] => [ - NO_CREDENTIAL_OPTION, + { ...NO_CREDENTIAL_OPTION, label: i18n.t(NO_CREDENTIAL_LABEL) }, ...credentials.map(toCredentialOption), ]; diff --git a/ui/litellm-dashboard/src/components/shared/date_range_presets.ts b/ui/litellm-dashboard/src/components/shared/date_range_presets.ts index 30a32f2519b..cad2be8e98a 100644 --- a/ui/litellm-dashboard/src/components/shared/date_range_presets.ts +++ b/ui/litellm-dashboard/src/components/shared/date_range_presets.ts @@ -1,7 +1,8 @@ +import type { ParseKeys } from "i18next"; import moment from "moment"; export interface RelativeTimeOption { - label: string; + label: ParseKeys; shortLabel: string; settingValue: string; getValue: () => { from: Date; to: Date }; @@ -9,7 +10,7 @@ export interface RelativeTimeOption { export const relativeTimeOptions: readonly RelativeTimeOption[] = [ { - label: "Today", + label: "shared.dateRangePresets.today", shortLabel: "today", settingValue: "today", getValue: () => ({ @@ -18,7 +19,7 @@ export const relativeTimeOptions: readonly RelativeTimeOption[] = [ }), }, { - label: "Last 7 days", + label: "shared.dateRangePresets.last7Days", shortLabel: "7d", settingValue: "last_7_days", getValue: () => ({ @@ -27,7 +28,7 @@ export const relativeTimeOptions: readonly RelativeTimeOption[] = [ }), }, { - label: "Last 30 days", + label: "shared.dateRangePresets.last30Days", shortLabel: "30d", settingValue: "last_30_days", getValue: () => ({ @@ -36,7 +37,7 @@ export const relativeTimeOptions: readonly RelativeTimeOption[] = [ }), }, { - label: "Month to date", + label: "shared.dateRangePresets.monthToDate", shortLabel: "MTD", settingValue: "month_to_date", getValue: () => ({ @@ -45,7 +46,7 @@ export const relativeTimeOptions: readonly RelativeTimeOption[] = [ }), }, { - label: "Year to date", + label: "shared.dateRangePresets.yearToDate", shortLabel: "YTD", settingValue: "year_to_date", getValue: () => ({ diff --git a/ui/litellm-dashboard/src/components/shared/search/SearchBox.tsx b/ui/litellm-dashboard/src/components/shared/search/SearchBox.tsx index ae6d5a6ec09..3b57fd20b5c 100644 --- a/ui/litellm-dashboard/src/components/shared/search/SearchBox.tsx +++ b/ui/litellm-dashboard/src/components/shared/search/SearchBox.tsx @@ -3,12 +3,14 @@ import "prosemirror-view/style/prosemirror.css"; import { useDebouncer } from "@tanstack/react-pacer/debouncer"; +import type { ParseKeys } from "i18next"; import { ProseMirror, ProseMirrorDoc, reactKeys, useEditorEventCallback } from "@handlewithcare/react-prosemirror"; import { Check, Copy, CornerDownLeft, Loader2, type LucideIcon, Search } from "lucide-react"; import { Schema } from "prosemirror-model"; import { EditorState, Plugin, TextSelection, type Transaction } from "prosemirror-state"; import { Decoration, DecorationSet, type EditorView } from "prosemirror-view"; import { type ComponentProps, createContext, useContext, useId, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Kbd } from "@/components/ui/kbd"; @@ -24,11 +26,11 @@ const schema = new Schema({ nodes: { doc: { content: "text*" }, text: {} } }); const EMIT_WAIT_MS = 150; const KEEP_MENU_CLOSED = "searchBoxKeepMenuClosed"; -const OPERATORS: readonly { readonly op: FilterOp; readonly label: string; readonly example: string }[] = [ - { op: "eq", label: "equals", example: "foo:bar" }, - { op: "neq", label: "not equals", example: "-foo:bar" }, - { op: "glob", label: "wildcard match", example: "foo:*bar*" }, - { op: "nglob", label: "does not contain", example: "-foo:*bar*" }, +const OPERATORS: readonly { readonly op: FilterOp; readonly label: ParseKeys; readonly example: string }[] = [ + { op: "eq", label: "shared.searchBox.operators.equals", example: "foo:bar" }, + { op: "neq", label: "shared.searchBox.operators.notEquals", example: "-foo:bar" }, + { op: "glob", label: "shared.searchBox.operators.wildcardMatch", example: "foo:*bar*" }, + { op: "nglob", label: "shared.searchBox.operators.doesNotContain", example: "-foo:*bar*" }, ]; /** Colours each `key:` so the filters stand out from free text. */ @@ -239,6 +241,7 @@ export type SearchBoxInputProps = ComponentProps<"div"> & { /** The bordered field holding the editor; shows `placeholder` while the query is empty. */ function Input({ placeholder, busy = false, className, ...props }: SearchBoxInputProps) { + const { t } = useTranslation(); const { text } = useSearchBox(); return (
) : ( @@ -277,6 +280,7 @@ export type SearchBoxSuggestionsProps = ComponentProps<"div">; * `children` render in the footer beside the key hints, e.g. a `CopyCommand`. */ function Suggestions({ className, children, ...props }: SearchBoxSuggestionsProps) { + const { t } = useTranslation(); const { listId, menu, activeId, icon, ops } = useSearchBox(); const pick = useEditorEventCallback((view, item: Suggestion) => applySuggestion(view, item)); if (!menu) return null; @@ -289,7 +293,12 @@ function Suggestions({ className, children, ...props }: SearchBoxSuggestionsProp )} {...props} > -
+
{menu.groups.map((group) => (
{group.heading}
@@ -314,7 +323,7 @@ function Suggestions({ className, children, ...props }: SearchBoxSuggestionsProp ))} {menu.loading && (
- Loading values… + {t("shared.searchBox.loadingValues")}
)} {menu.showOperators && } @@ -323,13 +332,13 @@ function Suggestions({ className, children, ...props }: SearchBoxSuggestionsProp ↑ ↓ - Navigate + {t("shared.searchBox.navigate")} - Select + {t("shared.searchBox.select")} {children}
@@ -346,13 +355,14 @@ export type SearchBoxCopyCommandProps = { type CopyOutcome = { readonly command: string; readonly outcome: "copied" | "failed" } | null; -const copyLabel = (last: CopyOutcome, command: string): string => { - if (last?.command !== command) return "Copy as curl"; - return last.outcome === "copied" ? "Copied" : "Copy failed"; +const copyLabel = (last: CopyOutcome, command: string): ParseKeys => { + if (last?.command !== command) return "shared.searchBox.copyCommand.copy"; + return last.outcome === "copied" ? "shared.searchBox.copyCommand.copied" : "shared.searchBox.copyCommand.failed"; }; /** Puts the current query on the clipboard as an API call, from the suggestions footer. */ function CopyCommand({ title, command }: SearchBoxCopyCommandProps) { + const { t } = useTranslation(); const { clauses } = useSearchBox(); const [last, setLast] = useState(null); const current = useMemo(() => command(toSearchQuery(clauses as readonly QueryClause[])), [command, clauses]); @@ -373,19 +383,20 @@ function CopyCommand({ title, command }: SearchBoxCopyCommandP onMouseDown={(event) => event.preventDefault()} onClick={() => void copy()} > - {label === "Copied" ? : } - {label} + {label === "shared.searchBox.copyCommand.copied" ? : } + {t(label)} ); } function OperatorHints({ ops }: { ops: readonly FilterOp[] }) { + const { t } = useTranslation(); return (
-
Comparison operators
+
{t("shared.searchBox.comparisonOperators")}
{OPERATORS.filter((op) => ops.includes(op.op)).map((op) => (
- {op.label} + {t(op.label)} {op.example}
))} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx index ee4c01bb237..ae61bbd76af 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx +++ b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx @@ -1,8 +1,25 @@ "use client"; +import type { ParseKeys } from "i18next"; + +import i18n from "@/i18n/i18n"; + import { CellTooltip } from "./cell_tooltip"; -const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const; +const MONTHS: readonly ParseKeys[] = [ + "shared.tableCells.dateCell.months.jan", + "shared.tableCells.dateCell.months.feb", + "shared.tableCells.dateCell.months.mar", + "shared.tableCells.dateCell.months.apr", + "shared.tableCells.dateCell.months.may", + "shared.tableCells.dateCell.months.jun", + "shared.tableCells.dateCell.months.jul", + "shared.tableCells.dateCell.months.aug", + "shared.tableCells.dateCell.months.sep", + "shared.tableCells.dateCell.months.oct", + "shared.tableCells.dateCell.months.nov", + "shared.tableCells.dateCell.months.dec", +]; export type DatePrecision = "datetime" | "date"; @@ -16,12 +33,12 @@ const pad = (n: number): string => String(n).padStart(2, "0"); export const formatCellDate = (date: Date, precision: DatePrecision): string => precision === "date" - ? `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}` - : `${MONTHS[date.getMonth()]} ${date.getDate()}, ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; + ? `${i18n.t(MONTHS[date.getMonth()])} ${date.getDate()}, ${date.getFullYear()}` + : `${i18n.t(MONTHS[date.getMonth()])} ${date.getDate()}, ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; export const formatFullTimestamp = (date: Date): string => { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const day = `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; + const day = `${i18n.t(MONTHS[date.getMonth()])} ${date.getDate()}, ${date.getFullYear()}`; const time = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; return `${day}, ${time} (${timeZone})`; }; diff --git a/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx index b4f051baa68..384d489b9f3 100644 --- a/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx +++ b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx @@ -3,6 +3,7 @@ import { X } from "lucide-react"; import moment from "moment"; import { useRef, useState, type RefObject } from "react"; +import { useTranslation } from "react-i18next"; import { useResizeObserver } from "usehooks-ts"; import { DotFieldCanvas, DotFieldRoot } from "@/components/shared/dotField/DotField"; @@ -85,6 +86,7 @@ function BucketTooltip({ bucketCount: number; noun: ItemNoun; }) { + const { t } = useTranslation(); return (
- {moment(bucket.startMs).format(EDGE_FORMAT)} to {moment(bucket.endMs).format("HH:mm")} + {t("shared.timeline.tooltip.range", { + start: moment(bucket.startMs).format(EDGE_FORMAT), + end: moment(bucket.endMs).format("HH:mm"), + })}
{bucket.total} {bucket.total === 1 ? noun.singular : noun.plural} - {bucket.failed > 0 && `, ${bucket.failed} failed`} + {bucket.failed > 0 && t("shared.timeline.tooltip.failed", { count: bucket.failed })}
-
drag to zoom
+
{t("shared.timeline.tooltip.dragToZoom")}
); } @@ -228,6 +233,7 @@ function TickAxis({ range, width }: { range: TimeWindow; width: number }) { /** Histogram over the window covered by `buckets`. Drag to select; drag the bracket or its edges to adjust; Esc clears. */ export function Timeline({ buckets, selection, onSelect, noun }: TimelineProps) { + const { t } = useTranslation(); const bucketCount = buckets.length; const [hover, setHover] = useState(null); const [drag, setDrag] = useState(null); @@ -340,7 +346,7 @@ export function Timeline({ buckets, selection, onSelect, noun }: TimelineProps)