feat(ui): i18n for entity usage export

This commit is contained in:
吹雪 2026-06-11 04:36:28 +09:00
parent 663681bc4e
commit 7ff15f970e
7 changed files with 125 additions and 32 deletions

View file

@ -2,6 +2,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { createTeamAliasMap } from "@/utils/teamUtils";
import { Button, Modal, Skeleton } from "antd";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
import ExportFormatSelector from "./ExportFormatSelector";
import ExportSummary from "./ExportSummary";
@ -18,13 +19,14 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
selectedFilters,
customTitle,
}) => {
const { t } = useTranslation();
const [exportFormat, setExportFormat] = useState<ExportFormat>("csv");
const [exportScope, setExportScope] = useState<ExportScope>("daily");
const [isExporting, setIsExporting] = useState(false);
const { data: teams, isLoading: isLoadingTeams } = useTeams();
const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
const modalTitle = customTitle || `Export ${entityLabel} Usage`;
const modalTitle = customTitle || t("usageExport.entityUsageExportModal.exportTitle", { entityLabel });
// Cache team alias map using useMemo
const teamAliasMap = useMemo(() => createTeamAliasMap(teams), [teams]);
@ -34,15 +36,15 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
try {
if (formatToUse === "csv") {
handleExportCSV(spendData, exportScope, entityLabel, entityType, teamAliasMap);
NotificationsManager.success(`${entityLabel} usage data exported successfully as CSV`);
NotificationsManager.success(t("usageExport.entityUsageExportModal.exportSuccessCsv", { entityLabel }));
} else {
handleExportJSON(spendData, exportScope, entityLabel, entityType, dateRange, selectedFilters, teamAliasMap);
NotificationsManager.success(`${entityLabel} usage data exported successfully as JSON`);
NotificationsManager.success(t("usageExport.entityUsageExportModal.exportSuccessJson", { entityLabel }));
}
onClose();
} catch (error) {
console.error("Error exporting data:", error);
NotificationsManager.fromBackend("Failed to export data");
NotificationsManager.fromBackend(t("usageExport.entityUsageExportModal.exportFailed"));
} finally {
setIsExporting(false);
}
@ -74,7 +76,7 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
) : (
<div className="flex items-center justify-end gap-2 pt-4 border-t">
<Button variant="outlined" onClick={onClose} disabled={isExporting}>
Cancel
{t("common.cancel")}
</Button>
<Button
onClick={() => handleExport()}
@ -82,7 +84,9 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
disabled={isExporting || isLoadingTeams}
type="primary"
>
{isExporting ? "Exporting..." : `Export ${exportFormat.toUpperCase()}`}
{isExporting
? t("usageExport.entityUsageExportModal.exporting")
: t("usageExport.entityUsageExportModal.exportFormat", { format: exportFormat.toUpperCase() })}
</Button>
</div>
)}

View file

@ -1,4 +1,6 @@
import React from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { Select } from "antd";
import type { ExportFormat } from "./types";
@ -7,25 +9,27 @@ interface ExportFormatSelectorProps {
onChange: (value: ExportFormat) => void;
}
const getFormatOptions = (t: TFunction) => [
{
value: "csv",
label: t("usageExport.exportFormatSelector.optionCsv"),
},
{
value: "json",
label: t("usageExport.exportFormatSelector.optionJson"),
},
];
const ExportFormatSelector: React.FC<ExportFormatSelectorProps> = ({ value, onChange }) => {
const { t } = useTranslation();
const formatOptions = useMemo(() => getFormatOptions(t), [t]);
return (
<div>
<label className="text-sm font-medium text-gray-700 block mb-2">Format</label>
<Select
value={value}
onChange={onChange}
className="w-full"
options={[
{
value: "csv",
label: "CSV (Excel, Google Sheets)",
},
{
value: "json",
label: "JSON (includes metadata)",
},
]}
/>
<label className="text-sm font-medium text-gray-700 block mb-2">
{t("usageExport.exportFormatSelector.label")}
</label>
<Select value={value} onChange={onChange} className="w-full" options={formatOptions} />
</div>
);
};

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import type { DateRangePickerValue } from "@tremor/react";
interface ExportSummaryProps {
@ -7,10 +8,13 @@ interface ExportSummaryProps {
}
const ExportSummary: React.FC<ExportSummaryProps> = ({ dateRange, selectedFilters }) => {
const { t } = useTranslation();
return (
<div className="text-sm text-gray-500">
{dateRange.from?.toLocaleDateString()} - {dateRange.to?.toLocaleDateString()}
{selectedFilters.length > 0 && ` · ${selectedFilters.length} filter${selectedFilters.length > 1 ? "s" : ""}`}
{selectedFilters.length > 0 &&
` · ${t("usageExport.exportSummary.filterCount", { count: selectedFilters.length })}`}
</div>
);
};

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Radio } from "antd";
import type { ExportScope, EntityType } from "./types";
@ -9,32 +10,48 @@ interface ExportTypeSelectorProps {
}
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange, entityType }) => {
const { t } = useTranslation();
return (
<div>
<label className="text-sm font-medium text-gray-700 block mb-2">Export type</label>
<label className="text-sm font-medium text-gray-700 block mb-2">
{t("usageExport.exportTypeSelector.label")}
</label>
<Radio.Group value={value} onChange={(e) => onChange(e.target.value)} className="w-full">
<div className="space-y-2">
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<Radio value="daily" className="mt-0.5" />
<div className="ml-3 flex-1">
<div className="font-medium text-sm">Day-by-day breakdown by {entityType}</div>
<div className="text-xs text-gray-500 mt-0.5">Daily metrics for each {entityType}</div>
<div className="font-medium text-sm">
{t("usageExport.exportTypeSelector.dailyTitle", { entityType })}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{t("usageExport.exportTypeSelector.dailyDesc", { entityType })}
</div>
</div>
</label>
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<Radio value="daily_with_keys" className="mt-0.5" />
<div className="ml-3 flex-1">
<div className="font-medium text-sm">Day-by-day breakdown by {entityType} and key</div>
<div className="text-xs text-gray-500 mt-0.5">Daily metrics for each {entityType}, split by API key</div>
<div className="font-medium text-sm">
{t("usageExport.exportTypeSelector.dailyWithKeysTitle", { entityType })}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{t("usageExport.exportTypeSelector.dailyWithKeysDesc", { entityType })}
</div>
</div>
</label>
<label className="flex items-start p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<Radio value="daily_with_models" className="mt-0.5" />
<div className="ml-3 flex-1">
<div className="font-medium text-sm">Day-by-day by {entityType} and model</div>
<div className="text-xs text-gray-500 mt-0.5">Daily metrics split by model</div>
<div className="font-medium text-sm">
{t("usageExport.exportTypeSelector.dailyWithModelsTitle", { entityType })}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{t("usageExport.exportTypeSelector.dailyWithModelsDesc")}
</div>
</div>
</label>
</div>

View file

@ -2,6 +2,7 @@ import type { DateRangePickerValue } from "@tremor/react";
import { Button, Text } from "@tremor/react";
import { Select } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import EntityUsageExportModal from "./EntityUsageExportModal";
import type { EntitySpendData, EntityType } from "./types";
import type { Team } from "@/components/key_team_helpers/key_list";
@ -38,6 +39,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
compactLayout = false,
teams = [],
}) => {
const { t } = useTranslation();
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
// Determine grid layout based on what's visible
@ -92,7 +94,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
</svg>
)}
>
Export Data
{t("usageExport.usageExportHeader.exportData")}
</Button>
</div>
</div>

View file

@ -7123,5 +7123,36 @@
"timezoneLabel": "Timezone",
"timezoneTooltip": "Timezone for date handling (defaults to UTC if not provided)"
}
},
"usageExport": {
"entityUsageExportModal": {
"exportTitle": "Export {{entityLabel}} Usage",
"exportSuccessCsv": "{{entityLabel}} usage data exported successfully as CSV",
"exportSuccessJson": "{{entityLabel}} usage data exported successfully as JSON",
"exportFailed": "Failed to export data",
"exporting": "Exporting...",
"exportFormat": "Export {{format}}"
},
"exportFormatSelector": {
"label": "Format",
"optionCsv": "CSV (Excel, Google Sheets)",
"optionJson": "JSON (includes metadata)"
},
"exportSummary": {
"filterCount_one": "{{count}} filter",
"filterCount_other": "{{count}} filters"
},
"exportTypeSelector": {
"label": "Export type",
"dailyTitle": "Day-by-day breakdown by {{entityType}}",
"dailyDesc": "Daily metrics for each {{entityType}}",
"dailyWithKeysTitle": "Day-by-day breakdown by {{entityType}} and key",
"dailyWithKeysDesc": "Daily metrics for each {{entityType}}, split by API key",
"dailyWithModelsTitle": "Day-by-day by {{entityType}} and model",
"dailyWithModelsDesc": "Daily metrics split by model"
},
"usageExportHeader": {
"exportData": "Export Data"
}
}
}

View file

@ -7123,5 +7123,36 @@
"timezoneLabel": "时区",
"timezoneTooltip": "日期处理时区(未提供时默认为 UTC)"
}
},
"usageExport": {
"entityUsageExportModal": {
"exportTitle": "导出 {{entityLabel}} 用量",
"exportSuccessCsv": "{{entityLabel}} 用量数据已成功导出为 CSV",
"exportSuccessJson": "{{entityLabel}} 用量数据已成功导出为 JSON",
"exportFailed": "导出数据失败",
"exporting": "导出中...",
"exportFormat": "导出 {{format}}"
},
"exportFormatSelector": {
"label": "格式",
"optionCsv": "CSV(Excel、Google Sheets)",
"optionJson": "JSON(包含元数据)"
},
"exportSummary": {
"filterCount_one": "{{count}} 个筛选器",
"filterCount_other": "{{count}} 个筛选器"
},
"exportTypeSelector": {
"label": "导出类型",
"dailyTitle": "按 {{entityType}} 逐日明细",
"dailyDesc": "每个 {{entityType}} 的每日指标",
"dailyWithKeysTitle": "按 {{entityType}} 和密钥逐日明细",
"dailyWithKeysDesc": "每个 {{entityType}} 的每日指标,按 API 密钥拆分",
"dailyWithModelsTitle": "按 {{entityType}} 和模型逐日明细",
"dailyWithModelsDesc": "按模型拆分的每日指标"
},
"usageExportHeader": {
"exportData": "导出数据"
}
}
}