diff --git a/ui/litellm-dashboard/src/app/(dashboard)/alerts/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/alerts/page.tsx index ec88a69670e..fe4692192d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/alerts/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/alerts/page.tsx @@ -1,14 +1,16 @@ "use client"; +import { useTranslation } from "react-i18next"; import { Page, PageTabs, PageTabsList, PageTabsTrigger, PageTabsContent } from "@/components/shared/Page"; import ModelDeprecationsPanel from "@/components/ModelDeprecationAlerts/ModelDeprecationsPanel"; export default function AlertsPage() { + const { t } = useTranslation(); return ( - Model deprecations + {t("alerts.page.tabs.modelDeprecations")} diff --git a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/AlertsInbox.tsx b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/AlertsInbox.tsx index f52af46ca30..58550a6a741 100644 --- a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/AlertsInbox.tsx +++ b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/AlertsInbox.tsx @@ -1,19 +1,27 @@ "use client"; import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import type { ColumnDef } from "@tanstack/react-table"; import { DataTable, DataTableToolbar } from "@/components/shared/DataTable"; import { PageTabs, PageTabsList, PageTabsTrigger, PageTabsContent } from "@/components/shared/Page"; +import i18n from "@/i18n/i18n"; import type { DeprecationAlert } from "./deprecationAlerts"; -import { AlertStatus, effectiveAlertStatus } from "./migrationState"; +import { ALERT_STATUS_LABEL_KEYS, AlertStatus, effectiveAlertStatus } from "./migrationState"; import RecommendedReplacement from "./RecommendedReplacement"; import { DeprecationModel, DeprecationTiming } from "./DeprecationModel"; import { migrationHref } from "./migrationRoute"; +const EMPTY_MESSAGE_KEYS = { + [AlertStatus.Open]: "alerts.inbox.empty.open", + [AlertStatus.Resolved]: "alerts.inbox.empty.resolved", + [AlertStatus.Dismissed]: "alerts.inbox.empty.dismissed", +} as const; + const columns: ColumnDef[] = [ { id: "model", - header: "Model", + header: () => i18n.t("alerts.inbox.columns.model"), cell: ({ row }) => (
@@ -23,7 +31,7 @@ const columns: ColumnDef[] = [ }, { id: "deprecation", - header: "Deprecation", + header: () => i18n.t("alerts.inbox.columns.deprecation"), cell: ({ row }) => (
@@ -31,14 +39,18 @@ const columns: ColumnDef[] = [
), }, - { accessorKey: "requests", header: "Requests · 30d", cell: ({ row }) => row.original.requests.toLocaleString() }, - { accessorKey: "lastUsedDate", header: "Last seen (UTC)" }, + { + accessorKey: "requests", + header: () => i18n.t("alerts.inbox.columns.requests"), + cell: ({ row }) => row.original.requests.toLocaleString(), + }, + { accessorKey: "lastUsedDate", header: () => i18n.t("alerts.inbox.columns.lastSeen") }, { id: "review", header: "", cell: ({ row }) => ( - Review impact → + {i18n.t("alerts.inbox.reviewImpact")} ), }, @@ -59,6 +71,7 @@ export default function AlertsInbox({ onRefresh: () => void; refreshing: boolean; }) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const counts = { open: 0, resolved: 0, dismissed: 0 }; for (const alert of alerts) counts[effectiveAlertStatus(alert)]++; @@ -75,7 +88,7 @@ export default function AlertsInbox({ {Object.values(AlertStatus).map((value) => ( - {value} + {t(ALERT_STATUS_LABEL_KEYS[value])} {counts[value]} ))} @@ -88,14 +101,14 @@ export default function AlertsInbox({ fillHeight size="compact" isLoading={pending} - loadingMessage="Loading model deprecations…" - noDataMessage={`No ${status} model deprecations in the last 30 days.`} + loadingMessage={t("alerts.inbox.loading")} + noDataMessage={t(EMPTY_MESSAGE_KEYS[status])} toolbar={(table) => (
@@ -22,13 +24,13 @@ export default function DeprecationAlertItem({ alert }: { alert: DeprecationAler

- {alert.requests.toLocaleString()} requests in the last 30 days · Last seen {alert.lastUsedDate} UTC + {t("alerts.alertItem.usage", { requests: alert.requests.toLocaleString(), date: alert.lastUsedDate })}

- Review migration + {t("alerts.alertItem.reviewMigration")} ); diff --git a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationEmailSetup.tsx b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationEmailSetup.tsx index b23fd5adadb..0d7185a6669 100644 --- a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationEmailSetup.tsx +++ b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationEmailSetup.tsx @@ -1,6 +1,8 @@ "use client"; +import type { ParseKeys } from "i18next"; import { Mail } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, @@ -14,44 +16,40 @@ import { import { uiHref } from "@/utils/uiHref"; const emailSettingsHref = `${uiHref("logging-and-alerts")}?tab=email-alerts`; -const setupSteps = [ +const setupSteps: readonly { titleKey: ParseKeys; descriptionKey: ParseKeys }[] = [ { - title: "Connect email delivery", - description: - "Add your SMTP settings and public gateway URL in Email Settings. Send a test email to verify delivery.", + titleKey: "alerts.emailSetup.steps.connect.title", + descriptionKey: "alerts.emailSetup.steps.connect.description", }, { - title: "Enable model deprecation emails", - description: "In Email events, turn on Model Deprecations and save. This is off by default.", + titleKey: "alerts.emailSetup.steps.enable.title", + descriptionKey: "alerts.emailSetup.steps.enable.description", }, { - title: "Owners get two reminders", - description: - "Affected key owners and team admins receive a reminder 30 days before retirement, and one final reminder 1 day before. Recipients are based on actual usage in the last 30 days. Dates are checked in UTC.", + titleKey: "alerts.emailSetup.steps.reminders.title", + descriptionKey: "alerts.emailSetup.steps.reminders.description", }, ]; export default function DeprecationEmailSetup() { + const { t } = useTranslation(); return (
- Give affected owners time to migrate. Email reminders at 30 days and 1 day. + {t("alerts.emailSetup.summary")} - }>Set up email reminders + }>{t("alerts.emailSetup.trigger")}
- Set up model deprecation emails - - Help key owners and team admins act before a provider retires a model. Only their affected usage is - included. - + {t("alerts.emailSetup.title")} + {t("alerts.emailSetup.description")}
    {setupSteps.map((step, index) => ( -
  1. +
  2. -

    {step.title}

    -

    {step.description}

    +

    {t(step.titleKey)}

    +

    {t(step.descriptionKey)}

  3. ))}
- Open Email Settings + {t("alerts.emailSetup.openSettings")}
diff --git a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationModel.tsx b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationModel.tsx index dfb0e2b71c3..70f19e5fc85 100644 --- a/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationModel.tsx +++ b/ui/litellm-dashboard/src/components/ModelDeprecationAlerts/DeprecationModel.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import type { DeprecationAlert } from "./deprecationAlerts"; import { migrationHref } from "./migrationRoute"; @@ -17,10 +18,11 @@ export function DeprecationModel({ alert }: { alert: DeprecationAlert }) { } export function DeprecationTiming({ alert }: { alert: DeprecationAlert }) { + const { t } = useTranslation(); const days = alert.model.days_until_deprecation; - let label = `Deprecates in ${days} ${days === 1 ? "day" : "days"}`; - if (days === 0) label = "Deprecates today"; - if (days < 0) label = `Deprecated ${Math.abs(days)} ${days === -1 ? "day" : "days"} ago`; + let label = t("alerts.timing.deprecatesIn", { count: days }); + if (days === 0) label = t("alerts.timing.today"); + if (days < 0) label = t("alerts.timing.deprecatedAgo", { count: Math.abs(days) }); return ( ; isViewOnly: boolean; }) { + const { t } = useTranslation(); const [params, setParams] = useQueryStates(migrationUrlParsers); const { data: teams = [] } = useTeams(); const teamNames = useMemo( @@ -144,27 +151,30 @@ export default function MigrationDetail({ () => [ { id: "key", - header: "Virtual key", + header: t("alerts.migrationDetail.columns.key"), cell: ({ row }) => ( - {row.original.metadata.key_alias || "Unnamed key"} + {row.original.metadata.key_alias || t("alerts.migrationDetail.unnamedKey")} ), }, { id: "team", - header: "Team", + header: t("alerts.migrationDetail.columns.team"), cell: ({ row }) => - teamNames.get(row.original.metadata.team_id ?? "") || row.original.metadata.team_id || "No team", + teamNames.get(row.original.metadata.team_id ?? "") || + row.original.metadata.team_id || + t("alerts.migrationDetail.noTeam"), }, { id: "owner", - header: "Owner", - cell: ({ row }) => row.original.metadata.user_email || row.original.metadata.user_id || "Not recorded", + header: t("alerts.migrationDetail.columns.owner"), + cell: ({ row }) => + row.original.metadata.user_email || row.original.metadata.user_id || t("alerts.migrationDetail.notRecorded"), }, { id: "tags", - header: "Usage tags", + header: t("alerts.migrationDetail.columns.tags"), cell: ({ row }) => ( {row.original.metrics.api_requests.toLocaleString()}, }, - { id: "spend", header: "Spend · 30d", cell: ({ row }) => `$${row.original.metrics.spend.toFixed(2)}` }, + { + id: "spend", + header: t("alerts.migrationDetail.columns.spend"), + cell: ({ row }) => `$${row.original.metrics.spend.toFixed(2)}`, + }, ], - [teamNames, tagsByKey, tagQuery.isPending, tagQuery.isError], + [teamNames, tagsByKey, tagQuery.isPending, tagQuery.isError, t], ); const status = effectiveAlertStatus(selected); - const heading = migrationHeading(selected, status); + const heading = migrationHeading(selected, status, t); const copyFollowUp = () => { const teamName = params.team ? teamNames.get(params.team) || params.team : "Team owners"; const count = keyQuery.data?.total_api_keys; @@ -192,23 +206,28 @@ export default function MigrationDetail({ : selected.lastUsedDate; const context = { team: teamName, count, lastSeen, url: window.location.href }; const message = migrationFollowUp(selected, context); - void copyToClipboard(message, "Follow-up copied"); + void copyToClipboard(message, t("alerts.migrationDetail.followUpCopied")); }; return ( <>
- {status} + + {t(ALERT_STATUS_LABEL_KEYS[status])} +

{heading}

- Last seen {selected.lastUsedDate} UTC · {selected.requests.toLocaleString()} requests in the last 30 days + {t("alerts.migrationDetail.lastSeen", { + date: selected.lastUsedDate, + requests: selected.requests.toLocaleString(), + })}

{!isViewOnly && ( <> @@ -219,7 +238,7 @@ export default function MigrationDetail({ onClick={() => statusMutation.mutate(AlertStatus.Resolved)} > - Mark resolved + {t("alerts.migrationDetail.markResolved")} ) : ( )} )} {modelGroupHref(selected.model.model_name) && ( - Model routing + {t("alerts.migrationDetail.modelRouting")} )}
-

- Resolve across all teams once owners confirm migration. New traffic reopens the alert. -

+

{t("alerts.migrationDetail.resolveHint")}

{statusMutation.isError && (

- Could not save alert status. Please retry. + {t("alerts.migrationDetail.saveError")}

)}
-

{params.team ? "Keys in this team" : "Affected keys"}

+

+ {params.team + ? t("alerts.migrationDetail.summary.teamKeys") + : t("alerts.migrationDetail.summary.affectedKeys")} +

{keyQuery.data?.total_api_keys.toLocaleString() ?? "—"}

-

Teams using this model

+

{t("alerts.migrationDetail.summary.teams")}

{teamQuery.data?.length ?? "—"}

-

Deprecation

+

{t("alerts.migrationDetail.summary.deprecation")}

@@ -276,14 +297,12 @@ export default function MigrationDetail({

- Affected keys{" "} + {t("alerts.migrationDetail.keysTable.title")}{" "} {keyQuery.data?.total_api_keys.toLocaleString() ?? "…"}

-

- Keys and owners that used this model in the last 30 days. Tags show actual usage with this model. -

+

{t("alerts.migrationDetail.keysTable.description")}