feat: add internationalization support with next-intl

This commit is contained in:
beilunyang
2025-10-13 00:57:32 +08:00
parent 0fcc4b9e85
commit d175017b51
46 changed files with 1436 additions and 432 deletions
+50 -45
View File
@@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Key, Plus, Loader2, Copy, Trash2, ChevronDown, ChevronUp } from "lucide-react"
@@ -32,6 +33,10 @@ type ApiKey = {
}
export function ApiKeyPanel() {
const t = useTranslations("profile.apiKey")
const tCommon = useTranslations("common.actions")
const tNoPermission = useTranslations("emails.noPermission")
const tMessages = useTranslations("emails.messages")
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [loading, setLoading] = useState(false)
const [createDialogOpen, setCreateDialogOpen] = useState(false)
@@ -47,14 +52,14 @@ export function ApiKeyPanel() {
const fetchApiKeys = async () => {
try {
const res = await fetch("/api/api-keys")
if (!res.ok) throw new Error("获取 API Keys 失败")
if (!res.ok) throw new Error(t("createFailed"))
const data = await res.json() as { apiKeys: ApiKey[] }
setApiKeys(data.apiKeys)
} catch (error) {
console.error(error)
toast({
title: "获取失败",
description: "获取 API Keys 列表失败",
title: t("createFailed"),
description: t("createFailed"),
variant: "destructive"
})
} finally {
@@ -81,15 +86,15 @@ export function ApiKeyPanel() {
body: JSON.stringify({ name: newKeyName })
})
if (!res.ok) throw new Error("创建 API Key 失败")
if (!res.ok) throw new Error(t("createFailed"))
const data = await res.json() as { key: string }
setNewKey(data.key)
fetchApiKeys()
} catch (error) {
toast({
title: "创建失败",
description: error instanceof Error ? error.message : "请稍后重试",
title: t("createFailed"),
description: error instanceof Error ? error.message : t("createFailed"),
variant: "destructive"
})
setCreateDialogOpen(false)
@@ -112,7 +117,7 @@ export function ApiKeyPanel() {
body: JSON.stringify({ enabled })
})
if (!res.ok) throw new Error("更新失败")
if (!res.ok) throw new Error(t("createFailed"))
setApiKeys(keys =>
keys.map(key =>
@@ -122,8 +127,8 @@ export function ApiKeyPanel() {
} catch (error) {
console.error(error)
toast({
title: "更新失败",
description: "更新 API Key 状态失败",
title: t("createFailed"),
description: t("createFailed"),
variant: "destructive"
})
}
@@ -135,18 +140,18 @@ export function ApiKeyPanel() {
method: "DELETE"
})
if (!res.ok) throw new Error("删除失败")
if (!res.ok) throw new Error(t("deleteFailed"))
setApiKeys(keys => keys.filter(key => key.id !== id))
toast({
title: "删除成功",
description: "API Key 已删除"
title: t("deleteSuccess"),
description: t("deleteSuccess")
})
} catch (error) {
console.error(error)
toast({
title: "删除失败",
description: "删除 API Key 失败",
title: t("deleteFailed"),
description: t("deleteFailed"),
variant: "destructive"
})
}
@@ -157,7 +162,7 @@ export function ApiKeyPanel() {
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Key className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">API Keys</h2>
<h2 className="text-lg font-semibold">{t("title")}</h2>
</div>
{
canManageApiKey && (
@@ -165,17 +170,17 @@ export function ApiKeyPanel() {
<DialogTrigger asChild>
<Button className="gap-2" onClick={() => setCreateDialogOpen(true)}>
<Plus className="w-4 h-4" />
API Key
{t("create")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{newKey ? "API Key 创建成功" : "创建新的 API Key"}
{newKey ? t("createSuccess") : t("create")}
</DialogTitle>
{newKey && (
<DialogDescription className="text-destructive">
{t("description")}
</DialogDescription>
)}
</DialogHeader>
@@ -183,18 +188,18 @@ export function ApiKeyPanel() {
{!newKey ? (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Label>{t("name")}</Label>
<Input
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="为你的 API Key 起个名字"
placeholder={t("namePlaceholder")}
/>
</div>
</div>
) : (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>API Key</Label>
<Label>{t("key")}</Label>
<div className="flex gap-2">
<Input
value={newKey}
@@ -220,7 +225,7 @@ export function ApiKeyPanel() {
onClick={handleDialogClose}
disabled={loading}
>
{newKey ? "完成" : "取消"}
{newKey ? tCommon("ok") : tCommon("cancel")}
</Button>
</DialogClose>
{!newKey && (
@@ -231,7 +236,7 @@ export function ApiKeyPanel() {
{loading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
"创建"
t("create")
)}
</Button>
)}
@@ -245,11 +250,11 @@ export function ApiKeyPanel() {
{
!canManageApiKey ? (
<div className="text-center text-muted-foreground py-8">
<p> API Key</p>
<p className="mt-2"></p>
<p>{tNoPermission("needPermission")}</p>
<p className="mt-2">{tNoPermission("contactAdmin")}</p>
{
config?.adminContact && (
<p className="mt-2">{config.adminContact}</p>
<p className="mt-2">{tNoPermission("adminContact")}: {config.adminContact}</p>
)
}
</div>
@@ -261,7 +266,7 @@ export function ApiKeyPanel() {
<Loader2 className="w-6 h-6 text-primary animate-spin" />
</div>
<div>
<p className="text-sm text-muted-foreground">...</p>
<p className="text-sm text-muted-foreground">{tMessages("loading")}</p>
</div>
</div>
) : apiKeys.length === 0 ? (
@@ -270,9 +275,9 @@ export function ApiKeyPanel() {
<Key className="w-6 h-6 text-primary" />
</div>
<div>
<h3 className="text-lg font-medium"> API Keys</h3>
<h3 className="text-lg font-medium">{t("noKeys")}</h3>
<p className="text-sm text-muted-foreground mt-1">
&quot;API Key&quot; API Key
{t("description")}
</p>
</div>
</div>
@@ -286,7 +291,7 @@ export function ApiKeyPanel() {
<div className="space-y-1">
<div className="font-medium">{key.name}</div>
<div className="text-sm text-muted-foreground">
{new Date(key.createdAt).toLocaleString()}
{t("createdAt")}: {new Date(key.createdAt).toLocaleString()}
</div>
</div>
<div className="flex items-center gap-2">
@@ -312,14 +317,14 @@ export function ApiKeyPanel() {
onClick={() => setShowExamples(!showExamples)}
>
{showExamples ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
使
{t("viewDocs")}
</button>
{showExamples && (
<div className="rounded-lg border bg-card p-4 space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-sm font-medium"></div>
<div className="text-sm font-medium">{t("docs.getConfig")}</div>
<Button
variant="ghost"
size="icon"
@@ -339,7 +344,7 @@ export function ApiKeyPanel() {
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-sm font-medium"></div>
<div className="text-sm font-medium">{t("docs.generateEmail")}</div>
<Button
variant="ghost"
size="icon"
@@ -371,7 +376,7 @@ export function ApiKeyPanel() {
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-sm font-medium"></div>
<div className="text-sm font-medium">{t("docs.getEmails")}</div>
<Button
variant="ghost"
size="icon"
@@ -391,7 +396,7 @@ export function ApiKeyPanel() {
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-sm font-medium"></div>
<div className="text-sm font-medium">{t("docs.getMessages")}</div>
<Button
variant="ghost"
size="icon"
@@ -411,7 +416,7 @@ export function ApiKeyPanel() {
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-sm font-medium"></div>
<div className="text-sm font-medium">{t("docs.getMessage")}</div>
<Button
variant="ghost"
size="icon"
@@ -430,16 +435,16 @@ export function ApiKeyPanel() {
</div>
<div className="text-xs text-muted-foreground mt-4">
<p></p>
<p>{t("docs.notes")}</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li> YOUR_API_KEY API Key</li>
<li>/api/config </li>
<li>emailId </li>
<li>messageId </li>
<li>expiryTime 3600000186400000160480000070</li>
<li>domain /api/config </li>
<li>cursor nextCursor</li>
<li> X-API-Key </li>
<li>{t("docs.note1")}</li>
<li>{t("docs.note2")}</li>
<li>{t("docs.note3")}</li>
<li>{t("docs.note4")}</li>
<li>{t("docs.note5")}</li>
<li>{t("docs.note6")}</li>
<li>{t("docs.note7")}</li>
<li>{t("docs.note8")}</li>
</ul>
</div>
</div>