feat(i18n): add date-fns locale support

This commit is contained in:
JiPai
2025-03-08 16:14:13 +08:00
parent 6df680e9da
commit 3a0dbc26d1
5 changed files with 257 additions and 82 deletions

View File

@@ -33,6 +33,7 @@ import {
import { Switch } from "@/components/ui/switch";
import copy from "copy-to-clipboard";
import { CodeEditor } from "@/components/shared/code-editor";
import { useTranslation } from "next-i18next";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -79,6 +80,7 @@ const REFILL_INTERVAL_OPTIONS = [
];
export const AddApiKey = () => {
const { t } = useTranslation('settings');
const [open, setOpen] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [newApiKey, setNewApiKey] = useState("");
@@ -95,7 +97,7 @@ export const AddApiKey = () => {
void refetch();
},
onError: () => {
toast.error("Failed to generate API key");
toast.error(t("settings.api.errorGeneratingApiKey"));
},
});
@@ -140,14 +142,13 @@ export const AddApiKey = () => {
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Generate New Key</Button>
<Button>{t("settings.api.generateNewKey")}</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Generate API Key</DialogTitle>
<DialogTitle>{t("settings.api.generateApiKey")}</DialogTitle>
<DialogDescription>
Create a new API key for accessing the API. You can set an
expiration date and a custom prefix for better organization.
{t("settings.api.createNewApiKeyDescription")}
</DialogDescription>
</DialogHeader>
<Form {...form}>
@@ -157,9 +158,9 @@ export const AddApiKey = () => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("settings.api.name")}</FormLabel>
<FormControl>
<Input placeholder="My API Key" {...field} />
<Input placeholder={t("settings.api.namePlaceholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -170,9 +171,9 @@ export const AddApiKey = () => {
name="prefix"
render={({ field }) => (
<FormItem>
<FormLabel>Prefix</FormLabel>
<FormLabel>{t("settings.api.prefix")}</FormLabel>
<FormControl>
<Input placeholder="my_app" {...field} />
<Input placeholder={t("settings.api.prefixPlaceholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -183,7 +184,7 @@ export const AddApiKey = () => {
name="expiresIn"
render={({ field }) => (
<FormItem>
<FormLabel>Expiration</FormLabel>
<FormLabel>{t("settings.api.expiration")}</FormLabel>
<Select
value={field.value?.toString() || "0"}
onValueChange={(value) =>
@@ -192,13 +193,13 @@ export const AddApiKey = () => {
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select expiration time" />
<SelectValue placeholder={t("settings.api.selectExpirationTime")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{EXPIRATION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(`settings.api.expirationOptions.${option.label}`)}
</SelectItem>
))}
</SelectContent>
@@ -212,11 +213,11 @@ export const AddApiKey = () => {
name="organizationId"
render={({ field }) => (
<FormItem>
<FormLabel>Organization</FormLabel>
<FormLabel>{t("settings.api.organization")}</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select organization" />
<SelectValue placeholder={t("settings.api.selectOrganization")} />
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -234,16 +235,16 @@ export const AddApiKey = () => {
{/* Rate Limiting Section */}
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-medium">Rate Limiting</h3>
<h3 className="text-lg font-medium">{t("settings.api.rateLimiting")}</h3>
<FormField
control={form.control}
name="rateLimitEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Enable Rate Limiting</FormLabel>
<FormLabel>{t("settings.api.enableRateLimiting")}</FormLabel>
<FormDescription>
Limit the number of requests within a time window
{t("settings.api.limitRequestsDescription")}
</FormDescription>
</div>
<FormControl>
@@ -263,7 +264,7 @@ export const AddApiKey = () => {
name="rateLimitTimeWindow"
render={({ field }) => (
<FormItem>
<FormLabel>Time Window</FormLabel>
<FormLabel>{t("settings.api.timeWindow")}</FormLabel>
<Select
value={field.value?.toString()}
onValueChange={(value) =>
@@ -272,7 +273,7 @@ export const AddApiKey = () => {
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select time window" />
<SelectValue placeholder={t("settings.api.selectTimeWindow")} />
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -281,13 +282,13 @@ export const AddApiKey = () => {
key={option.value}
value={option.value}
>
{option.label}
{t(`settings.api.timeWindowOptions.${option.label}`)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
The duration in which requests are counted
{t("settings.api.timeWindowDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -298,11 +299,11 @@ export const AddApiKey = () => {
name="rateLimitMax"
render={({ field }) => (
<FormItem>
<FormLabel>Maximum Requests</FormLabel>
<FormLabel>{t("settings.api.maxRequests")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="100"
placeholder={t("settings.api.maxRequestsPlaceholder")}
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
@@ -314,8 +315,7 @@ export const AddApiKey = () => {
/>
</FormControl>
<FormDescription>
Maximum number of requests allowed within the time
window
{t("settings.api.maxRequestsDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -327,17 +327,17 @@ export const AddApiKey = () => {
{/* Request Limiting Section */}
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-medium">Request Limiting</h3>
<h3 className="text-lg font-medium">{t("settings.api.requestLimiting")}</h3>
<FormField
control={form.control}
name="remaining"
render={({ field }) => (
<FormItem>
<FormLabel>Total Request Limit</FormLabel>
<FormLabel>{t("settings.api.totalRequestLimit")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Leave empty for unlimited"
placeholder={t("settings.api.totalRequestLimitPlaceholder")}
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
@@ -349,8 +349,7 @@ export const AddApiKey = () => {
/>
</FormControl>
<FormDescription>
Total number of requests allowed (leave empty for
unlimited)
{t("settings.api.totalRequestLimitDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -362,11 +361,11 @@ export const AddApiKey = () => {
name="refillAmount"
render={({ field }) => (
<FormItem>
<FormLabel>Refill Amount</FormLabel>
<FormLabel>{t("settings.api.refillAmount")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Amount to refill"
placeholder={t("settings.api.refillAmountPlaceholder")}
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
@@ -378,7 +377,7 @@ export const AddApiKey = () => {
/>
</FormControl>
<FormDescription>
Number of requests to add on each refill
{t("settings.api.refillAmountDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -390,7 +389,7 @@ export const AddApiKey = () => {
name="refillInterval"
render={({ field }) => (
<FormItem>
<FormLabel>Refill Interval</FormLabel>
<FormLabel>{t("settings.api.refillInterval")}</FormLabel>
<Select
value={field.value?.toString()}
onValueChange={(value) =>
@@ -399,19 +398,19 @@ export const AddApiKey = () => {
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select refill interval" />
<SelectValue placeholder={t("settings.api.selectRefillInterval")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{REFILL_INTERVAL_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(`settings.api.refillIntervalOptions.${option.label}`)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
How often to refill the request limit
{t("settings.api.refillIntervalDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -425,9 +424,9 @@ export const AddApiKey = () => {
variant="outline"
onClick={() => setOpen(false)}
>
Cancel
{t("settings.api.cancel")}
</Button>
<Button type="submit">Generate</Button>
<Button type="submit">{t("settings.api.generate")}</Button>
</div>
</form>
</Form>
@@ -437,9 +436,9 @@ export const AddApiKey = () => {
<Dialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>API Key Generated Successfully</DialogTitle>
<DialogTitle>{t("settings.api.apiKeyGeneratedSuccessfully")}</DialogTitle>
<DialogDescription>
Please copy your API key now. You won't be able to see it again!
{t("settings.api.copyApiKeyNow")}
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
@@ -453,16 +452,16 @@ export const AddApiKey = () => {
<Button
onClick={() => {
copy(newApiKey);
toast.success("API key copied to clipboard");
toast.success(t("settings.api.apiKeyCopied"));
}}
>
Copy to Clipboard
{t("settings.api.copyToClipboard")}
</Button>
<Button
variant="outline"
onClick={() => setShowSuccessModal(false)}
>
Close
{t("settings.api.close")}
</Button>
</div>
</div>

View File

@@ -14,8 +14,11 @@ import { formatDistanceToNow } from "date-fns";
import { DialogAction } from "@/components/shared/dialog-action";
import { AddApiKey } from "./add-api-key";
import { Badge } from "@/components/ui/badge";
import { useTranslation } from "next-i18next";
import { getDateFnsLocaleByCode } from "@/lib/languages";
export const ShowApiKeys = () => {
const { t, i18n } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } =
api.user.deleteApiKey.useMutation();
@@ -28,22 +31,24 @@ export const ShowApiKeys = () => {
<div>
<CardTitle className="text-xl flex items-center gap-2">
<KeyIcon className="size-5" />
API/CLI Keys
{t("settings.api.apiCliKeys")}
</CardTitle>
<CardDescription>
Generate and manage API keys to access the API/CLI
{t("settings.api.generateAndManageKeys")}
</CardDescription>
</div>
<div className="flex flex-row gap-2 max-sm:flex-wrap items-end">
<span className="text-sm font-medium text-muted-foreground">
Swagger API:
{t("settings.api.swaggerApi")}
</span>
<Link
href="/swagger"
target="_blank"
className="flex flex-row gap-2 items-center"
>
<span className="text-sm font-medium">View</span>
<span className="text-sm font-medium">
{t("settings.api.view")}
</span>
<ExternalLinkIcon className="size-4" />
</Link>
</div>
@@ -62,9 +67,11 @@ export const ShowApiKeys = () => {
<div className="flex flex-wrap gap-2 items-center text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="size-3.5" />
Created{" "}
{formatDistanceToNow(new Date(apiKey.createdAt))}{" "}
ago
{t("settings.api.created")}{" "}
{formatDistanceToNow(new Date(apiKey.createdAt), {
locale: getDateFnsLocaleByCode(i18n.language),
})}{" "}
{t("settings.api.ago")}
</span>
{apiKey.prefix && (
<Badge
@@ -81,17 +88,17 @@ export const ShowApiKeys = () => {
className="flex items-center gap-1"
>
<Clock className="size-3.5" />
Expires in{" "}
{formatDistanceToNow(
new Date(apiKey.expiresAt),
)}{" "}
{t("settings.api.expiresIn")}{" "}
{formatDistanceToNow(new Date(apiKey.expiresAt), {
locale: getDateFnsLocaleByCode(i18n.language),
})}{" "}
</Badge>
)}
</div>
</div>
<DialogAction
title="Delete API Key"
description="Are you sure you want to delete this API key? This action cannot be undone."
title={t("settings.api.deleteApiKey")}
description={t("settings.api.deleteApiKeyDescription")}
type="destructive"
onClick={async () => {
try {
@@ -99,12 +106,12 @@ export const ShowApiKeys = () => {
apiKeyId: apiKey.id,
});
await refetch();
toast.success("API key deleted successfully");
toast.success(t("settings.api.apiKeyDeleted"));
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Error deleting API key",
: t("settings.api.errorDeletingApiKey"),
);
}
}}
@@ -124,7 +131,7 @@ export const ShowApiKeys = () => {
<div className="flex flex-col items-center gap-3 py-6">
<KeyIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No API keys found
{t("settings.api.noApiKeysFound")}
</span>
</div>
)}