From 8a04f13dd4ef54c41fc7bc0fedc878d6488a0c0a Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 12 Aug 2026 10:37:19 -0400 Subject: [PATCH] translate keys and restructure roles and users --- messages/en-US.json | 56 +++++++++++++ .../[orgId]/settings/logs/ai-usage/page.tsx | 15 ++-- src/components/AiUsageAnalyticsData.tsx | 34 ++++---- .../ai-usage-analytics/OverviewTab.tsx | 78 ++++++++++--------- .../ai-usage-analytics/ProvidersTab.tsx | 24 +++--- .../ai-usage-analytics/ResourcesTab.tsx | 41 +++++----- .../ToggleableTrendChart.tsx | 48 +++++++----- .../ai-usage-analytics/TopEntitiesList.tsx | 18 +++-- .../ai-usage-analytics/UsersRolesTab.tsx | 66 +++++++++------- src/components/ai-usage-analytics/shared.ts | 5 +- src/components/ui/chart.tsx | 6 +- 11 files changed, 241 insertions(+), 150 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 166ca8a4b..bb1325ee9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1936,6 +1936,62 @@ "aiResourceAliasRequired": "Alias is required for inference resources", "aiResourceDomainConfiguration": "Domain configuration", "aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.", + "aiUsageAnalyticsTitle": "AI Usage Analytics", + "aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users", + "aiUsageTabOverview": "Overview", + "aiUsageTabProviders": "Providers", + "aiUsageTabResources": "Resources", + "aiUsageTabUsersRoles": "Users & Roles", + "aiUsageFilterProvider": "Provider", + "aiUsageFilterModel": "Model", + "aiUsageFilterResource": "Resource", + "aiUsageFilterRole": "Role", + "aiUsageFilterUser": "User", + "aiUsageFilterAllProviders": "All Providers", + "aiUsageFilterAllModels": "All Models", + "aiUsageFilterAllResources": "All Resources", + "aiUsageFilterAllRoles": "All Roles", + "aiUsageFilterAllUsers": "All Users", + "aiUsageResetFilters": "Reset Filters", + "aiUsageRefresh": "Refresh", + "aiUsageTokenTypePrompt": "Prompt", + "aiUsageTokenTypeCacheRead": "Cache read", + "aiUsageTokenTypeCacheWrite": "Cache write", + "aiUsageTokenTypeCompletion": "Completion", + "aiUsageTokenTypeReasoning": "Reasoning", + "aiUsageRequests": "Requests", + "aiUsageCost": "Cost", + "aiUsageTokens": "Tokens", + "aiUsageOther": "Other", + "aiUsageTotalRequests": "Total Requests", + "aiUsageTotalTokens": "Total Tokens", + "aiUsageTotalCost": "Total Cost", + "aiUsageEstimated": "Estimated", + "aiUsageRequestVolume": "Request Volume", + "aiUsageTokenUsage": "Token Usage", + "aiUsageModelCost": "Model Cost", + "aiUsageModelTokens": "Model Tokens", + "aiUsageTopModels": "Top Models", + "aiUsageTopProviders": "Top Providers", + "aiUsageProviderCost": "Provider Cost", + "aiUsageProviderTokenUsage": "Provider Token Usage", + "aiUsageTopResources": "Top Resources", + "aiUsageResourceCost": "Resource Cost", + "aiUsageResourceTokenUsage": "Resource Token Usage", + "aiUsageResourceTypePublic": "Resource", + "aiUsageResourceTypeSite": "Site resource", + "aiUsageNoResource": "No resource", + "aiUsageRolesTab": "Roles", + "aiUsageUsersTab": "Users", + "aiUsageTopRoles": "Top Roles", + "aiUsageRoleCost": "Role Cost", + "aiUsageRoleTokenUsage": "Role Token Usage", + "aiUsageTopUsers": "Top Users", + "aiUsageUserCost": "User Cost", + "aiUsageUserTokenUsage": "User Token Usage", + "aiUsageUnknownUser": "Unknown user", + "aiUsageLoading": "Loading...", + "aiUsageNoData": "No data", "resourceBudgetSettings": "Budget", "resourceBudgetSettingsDescription": "Configure how this inference resource restricts usage based on spending or token limits", "sidebarApiKeys": "API Keys", diff --git a/src/app/[orgId]/settings/logs/ai-usage/page.tsx b/src/app/[orgId]/settings/logs/ai-usage/page.tsx index f045aead0..72e9cea71 100644 --- a/src/app/[orgId]/settings/logs/ai-usage/page.tsx +++ b/src/app/[orgId]/settings/logs/ai-usage/page.tsx @@ -1,10 +1,14 @@ import { AiUsageAnalyticsData } from "@app/components/AiUsageAnalyticsData"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; -export const metadata: Metadata = { - title: "AI Usage Analytics" -}; +export async function generateMetadata(): Promise { + const t = await getTranslations(); + return { + title: t("aiUsageAnalyticsTitle") + }; +} export interface AiUsageAnalyticsPageProps { params: Promise<{ orgId: string }>; @@ -14,12 +18,13 @@ export default async function AiUsageAnalyticsPage( props: AiUsageAnalyticsPageProps ) { const orgId = (await props.params).orgId; + const t = await getTranslations(); return ( <>
diff --git a/src/components/AiUsageAnalyticsData.tsx b/src/components/AiUsageAnalyticsData.tsx index 879aff1bc..748fc7cdf 100644 --- a/src/components/AiUsageAnalyticsData.tsx +++ b/src/components/AiUsageAnalyticsData.tsx @@ -8,6 +8,7 @@ import { } from "@app/lib/queries"; import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, XIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { DateRangePicker, type DateTimeValue } from "./DateTimePicker"; import { Button } from "./ui/button"; @@ -35,6 +36,7 @@ export type AiUsageAnalyticsDataProps = { const AI_USAGE_ANALYTICS_QUERY_PREFIX = ["AI_USAGE_ANALYTICS"]; export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) { + const t = useTranslations(); const searchParams = useSearchParams(); const path = usePathname(); const router = useRouter(); @@ -139,10 +141,10 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) { })); const tabs: TabItem[] = [ - { title: "Overview", href: "#" }, - { title: "Provider Usage", href: "#" }, - { title: "Resources", href: "#" }, - { title: "Users & Roles", href: "#" } + { title: t("aiUsageTabOverview"), href: "#" }, + { title: t("aiUsageTabProviders"), href: "#" }, + { title: t("aiUsageTabResources"), href: "#" }, + { title: t("aiUsageTabUsersRoles"), href: "#" } ]; return ( @@ -172,42 +174,42 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
setFilter("providerId", v)} /> setFilter("model", v)} /> setFilter("resourceId", v)} /> setFilter("roleId", v)} /> setFilter("userId", v)} /> @@ -218,7 +220,7 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) { className="gap-2" > - Reset Filters + {t("aiUsageResetFilters")} )}
@@ -243,7 +245,7 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) { isFetching && "animate-spin" )} /> - Refresh + {t("aiUsageRefresh")}
diff --git a/src/components/ai-usage-analytics/OverviewTab.tsx b/src/components/ai-usage-analytics/OverviewTab.tsx index 1bf4dab1d..c539f11d9 100644 --- a/src/components/ai-usage-analytics/OverviewTab.tsx +++ b/src/components/ai-usage-analytics/OverviewTab.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; +import { useTranslations } from "next-intl"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { InfoSection, @@ -24,15 +25,8 @@ type OverviewTabProps = { filters: AiUsageAnalyticsFilters; }; -const TOKEN_TYPE_LABELS: Record = { - promptTokens: "Prompt", - cacheReadTokens: "Cache read", - cacheWriteTokens: "Cache write", - completionTokens: "Completion", - reasoningTokens: "Reasoning" -}; - export function OverviewTab(props: OverviewTabProps) { + const t = useTranslations(); const { data, isLoading } = useQuery( aiUsageAnalyticsQueries.overview({ orgId: props.orgId, @@ -40,8 +34,16 @@ export function OverviewTab(props: OverviewTabProps) { }) ); + const TOKEN_TYPE_LABELS: Record = { + promptTokens: t("aiUsageTokenTypePrompt"), + cacheReadTokens: t("aiUsageTokenTypeCacheRead"), + cacheWriteTokens: t("aiUsageTokenTypeCacheWrite"), + completionTokens: t("aiUsageTokenTypeCompletion"), + reasoningTokens: t("aiUsageTokenTypeReasoning") + }; + const requestsSeries = [ - { key: "requests", label: "Requests", color: SERIES_COLORS[0] } + { key: "requests", label: t("aiUsageRequests"), color: SERIES_COLORS[0] } ]; const tokensSeries = Object.keys(TOKEN_TYPE_LABELS).map((key, i) => ({ key, @@ -49,16 +51,18 @@ export function OverviewTab(props: OverviewTabProps) { color: SERIES_COLORS[i % SERIES_COLORS.length] })); const costSeries = [ - { key: "cost", label: "Cost", color: SERIES_COLORS[0] } + { key: "cost", label: t("aiUsageCost"), color: SERIES_COLORS[0] } ]; const modelCostSeries = buildSeriesFromData( data?.modelCostPerDay ?? [], - (key) => key + (key) => key, + t("aiUsageOther") ); const modelTokensSeries = buildSeriesFromData( data?.modelTokensPerDay ?? [], - (key) => key + (key) => key, + t("aiUsageOther") ); const topModels: TopEntity[] = (data?.topModels ?? []).map((m) => ({ @@ -75,7 +79,9 @@ export function OverviewTab(props: OverviewTabProps) { - Total Requests + + {t("aiUsageTotalRequests")} + {data ? compactNumberFormatter.format( @@ -85,7 +91,9 @@ export function OverviewTab(props: OverviewTabProps) { - Total Tokens + + {t("aiUsageTotalTokens")} + {data ? compactNumberFormatter.format( @@ -95,13 +103,17 @@ export function OverviewTab(props: OverviewTabProps) { - Total Cost + + {t("aiUsageTotalCost")} + {data ? formatCost(data.totalCost) : "--"} - Estimated + + {t("aiUsageEstimated")} + {data ? `${Math.round(data.estimatedPercent)}%` @@ -114,11 +126,9 @@ export function OverviewTab(props: OverviewTabProps) {
- -

Request Volume

-
- +
- -

Token Usage

-
- +
- -

Cost

-
- + - -

Model Cost

-
- +
- -

Model Tokens

-
- + -

Top Models

+

{t("aiUsageTopModels")}

diff --git a/src/components/ai-usage-analytics/ProvidersTab.tsx b/src/components/ai-usage-analytics/ProvidersTab.tsx index 70756d558..e3b7a5d29 100644 --- a/src/components/ai-usage-analytics/ProvidersTab.tsx +++ b/src/components/ai-usage-analytics/ProvidersTab.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; +import { useTranslations } from "next-intl"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { ToggleableTrendChart } from "./ToggleableTrendChart"; import { TopEntitiesList, type TopEntity } from "./TopEntitiesList"; @@ -14,6 +15,7 @@ type ProvidersTabProps = { }; export function ProvidersTab(props: ProvidersTabProps) { + const t = useTranslations(); const { data, isLoading } = useQuery( aiUsageAnalyticsQueries.providers({ orgId: props.orgId, @@ -29,11 +31,13 @@ export function ProvidersTab(props: ProvidersTabProps) { const costSeries = buildSeriesFromData( data?.providerCostPerDay ?? [], - labelFor + labelFor, + t("aiUsageOther") ); const tokensSeries = buildSeriesFromData( data?.providerTokensPerDay ?? [], - labelFor + labelFor, + t("aiUsageOther") ); const topProviders: TopEntity[] = (data?.topProviders ?? []).map((p) => ({ @@ -48,24 +52,22 @@ export function ProvidersTab(props: ProvidersTabProps) {
-

Top Providers

+

{t("aiUsageTopProviders")}

- -

Provider Cost

-
- +
- -

Provider Token Usage

-
- + - key === "none" ? "No resource" : (nameByKey.get(key) ?? key); + key === "none" ? t("aiUsageNoResource") : (nameByKey.get(key) ?? key); const costSeries = buildSeriesFromData( data?.resourceCostPerDay ?? [], - labelFor + labelFor, + t("aiUsageOther") ); const tokensSeries = buildSeriesFromData( data?.resourceTokensPerDay ?? [], - labelFor + labelFor, + t("aiUsageOther") ); const topResources: TopEntity[] = (data?.topResources ?? []).map((r) => ({ key: r.key, - label: r.name ?? (r.key === "none" ? "No resource" : r.key), + label: r.name ?? (r.key === "none" ? t("aiUsageNoResource") : r.key), sublabel: resourceTypeLabel(r.type), requests: r.requests, totalTokens: r.totalTokens, @@ -56,24 +61,22 @@ export function ResourcesTab(props: ResourcesTabProps) {
-

Top Resources

+

{t("aiUsageTopResources")}

- -

Resource Cost

-
- +
- -

Resource Token Usage

-
- + ("bar"); const valueFormatter = props.valueFormatter ?? compactFormatter.format; @@ -61,25 +64,28 @@ export function ToggleableTrendChart(props: ToggleableTrendChartProps) { return (
-
- - +
+

{props.title}

+
+ + +
{!hasData ? ( @@ -87,10 +93,10 @@ export function ToggleableTrendChart(props: ToggleableTrendChartProps) { {props.isLoading ? ( <> - Loading... + {t("aiUsageLoading")} ) : ( - "No data" + t("aiUsageNoData") )}
) : ( diff --git a/src/components/ai-usage-analytics/TopEntitiesList.tsx b/src/components/ai-usage-analytics/TopEntitiesList.tsx index 9f712363f..6a445db6c 100644 --- a/src/components/ai-usage-analytics/TopEntitiesList.tsx +++ b/src/components/ai-usage-analytics/TopEntitiesList.tsx @@ -1,6 +1,7 @@ "use client"; import { LoaderIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; import { compactNumberFormatter, formatCost } from "./shared"; export type TopEntity = { @@ -20,6 +21,7 @@ type TopEntitiesListProps = { }; export function TopEntitiesList(props: TopEntitiesListProps) { + const t = useTranslations(); const totalCost = props.entities.reduce( (sum, e) => sum + (e.costUsd ?? 0), 0 @@ -30,9 +32,15 @@ export function TopEntitiesList(props: TopEntitiesListProps) { {props.entities.length > 0 && (
{props.nameColumnLabel}
-
Requests
-
Tokens
-
Cost
+
+ {t("aiUsageRequests")} +
+
+ {t("aiUsageTokens")} +
+
+ {t("aiUsageCost")} +
%
)} @@ -42,10 +50,10 @@ export function TopEntitiesList(props: TopEntitiesListProps) { {props.isLoading ? ( <> - Loading... + {t("aiUsageLoading")} ) : ( - (props.emptyLabel ?? "No data") + (props.emptyLabel ?? t("aiUsageNoData")) )}
)} diff --git a/src/components/ai-usage-analytics/UsersRolesTab.tsx b/src/components/ai-usage-analytics/UsersRolesTab.tsx index 3d8d411f1..6e49ce9fb 100644 --- a/src/components/ai-usage-analytics/UsersRolesTab.tsx +++ b/src/components/ai-usage-analytics/UsersRolesTab.tsx @@ -3,7 +3,9 @@ import { useQuery } from "@tanstack/react-query"; import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; +import { useTranslations } from "next-intl"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { ToggleableTrendChart } from "./ToggleableTrendChart"; import { TopEntitiesList, type TopEntity } from "./TopEntitiesList"; import { buildSeriesFromData, formatCost } from "./shared"; @@ -16,6 +18,7 @@ type UsersRolesTabProps = { const UNKNOWN_USER_KEY = "unknown"; export function UsersRolesTab(props: UsersRolesTabProps) { + const t = useTranslations(); const { data, isLoading } = useQuery( aiUsageAnalyticsQueries.usersRoles({ orgId: props.orgId, @@ -38,24 +41,28 @@ export function UsersRolesTab(props: UsersRolesTabProps) { } const userLabelFor = (key: string) => key === UNKNOWN_USER_KEY - ? "Unknown user" + ? t("aiUsageUnknownUser") : (userEmailByKey.get(key) ?? key); const roleCostSeries = buildSeriesFromData( data?.roleCostPerDay ?? [], - roleLabelFor + roleLabelFor, + t("aiUsageOther") ); const roleTokensSeries = buildSeriesFromData( data?.roleTokensPerDay ?? [], - roleLabelFor + roleLabelFor, + t("aiUsageOther") ); const userCostSeries = buildSeriesFromData( data?.userCostPerDay ?? [], - userLabelFor + userLabelFor, + t("aiUsageOther") ); const userTokensSeries = buildSeriesFromData( data?.userTokensPerDay ?? [], - userLabelFor + userLabelFor, + t("aiUsageOther") ); const topRoles: TopEntity[] = (data?.topRoles ?? []).map((r) => ({ @@ -68,35 +75,39 @@ export function UsersRolesTab(props: UsersRolesTabProps) { const topUsers: TopEntity[] = (data?.topUsers ?? []).map((u) => ({ key: u.userId ?? UNKNOWN_USER_KEY, - label: u.email ?? u.userId ?? "Unknown user", + label: u.email ?? u.userId ?? t("aiUsageUnknownUser"), requests: u.requests, totalTokens: u.totalTokens, costUsd: u.costUsd })); + const tabs: TabItem[] = [ + { title: t("aiUsageRolesTab"), href: "#" }, + { title: t("aiUsageUsersTab"), href: "#" } + ]; + return ( -
+
-

Roles

-

Top Roles

+

+ {t("aiUsageTopRoles")} +

- -

Role Cost

-
- +
- -

Role Token Usage

-
- +
-

Users

-

Top Users

+

+ {t("aiUsageTopUsers")} +

- -

User Cost

-
- +
- -

User Token Usage

-
- +
-
+ ); } diff --git a/src/components/ai-usage-analytics/shared.ts b/src/components/ai-usage-analytics/shared.ts index 1f551f8fa..30ad525b4 100644 --- a/src/components/ai-usage-analytics/shared.ts +++ b/src/components/ai-usage-analytics/shared.ts @@ -18,7 +18,8 @@ export const OTHER_KEY = "other"; // top-list needed. Assigns one categorical color per key, "other" last. export function buildSeriesFromData( data: Array>, - labelFor: (key: string) => string + labelFor: (key: string) => string, + otherLabel = "Other" ): TrendSeries[] { const keys = new Set(); for (const row of data) { @@ -37,7 +38,7 @@ export function buildSeriesFromData( const hasOther = data.some((row) => OTHER_KEY in row); if (hasOther) { - series.push({ key: OTHER_KEY, label: "Other", color: OTHER_COLOR }); + series.push({ key: OTHER_KEY, label: otherLabel, color: OTHER_COLOR }); } return series; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 83535e6cc..168393b29 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -57,7 +57,11 @@ const ChartContainer = React.forwardRef< {...props} > - + {children}