From 4cd169f3bd9a4de14d780c5b7dd13a8694a9aaf6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Nov 2026 12:42:37 -0800 Subject: [PATCH] refactor(ui): move remaining shell and nav text onto i18n (#50148) Moves the leftover English in the sidebar, page descriptions, account menu, usage card and a handful of shared widgets into en.json. Option lists and label maps now hold message keys and are translated where they render, while every option value, id and stored setting stays the same string. Lookups keyed by a server value check Object.hasOwn first --- .../app/(dashboard)/hooks/useAuthorized.ts | 4 +- .../_components/CreateMCPServer.tsx | 9 +- .../mcp-servers/_components/MCPServerCard.tsx | 2 +- .../_components/StdioAvailability.tsx | 2 +- .../_components/mcp_server_edit.tsx | 9 +- .../components/AIHub/MCPHubTableColumns.tsx | 2 +- .../CommandPalette/CommandPalette.tsx | 3 +- .../DeploymentWarningContent.tsx | 15 +- .../components/Navbar/DashboardResources.tsx | 20 +- .../NotificationsBell/NotificationsBell.tsx | 12 +- .../AdminSettings/UISettings/UISettings.tsx | 4 +- .../SidebarAccountMenu/SidebarAccountMenu.tsx | 53 ++-- .../src/components/SidebarUsageCard.tsx | 35 +- .../src/components/key_scope.ts | 12 +- .../src/components/leftnav.tsx | 171 ++++++---- .../src/components/mcp_tools/constants.ts | 5 +- .../src/components/mcp_tools/types.tsx | 40 +-- .../src/components/networking.tsx | 16 +- .../src/components/page_metadata.ts | 86 ++--- .../src/components/page_utils.ts | 13 +- .../components/publicModelHub/hubFormat.ts | 14 +- .../src/components/shared/DurationInput.tsx | 14 +- .../components/shared/InheritedBudgetHint.tsx | 14 +- .../src/components/shared/Inspector.tsx | 19 +- .../src/components/shared/InspectorTable.tsx | 8 +- .../src/components/shared/ShortcutHints.tsx | 4 +- .../shared/advanced_date_picker.tsx | 2 +- .../components/shared/credentialOptions.ts | 9 +- .../components/shared/date_range_presets.ts | 13 +- .../components/shared/search/SearchBox.tsx | 45 ++- .../shared/table_cells/date_cell.tsx | 25 +- .../components/shared/timeline/Timeline.tsx | 14 +- ui/litellm-dashboard/src/i18n/locales/en.json | 298 ++++++++++++++++++ .../src/utils/licenseUtils.ts | 8 +- ui/litellm-dashboard/src/utils/roles.ts | 18 ++ 35 files changed, 763 insertions(+), 255 deletions(-) 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)