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
+24 -21
View File
@@ -1,5 +1,6 @@
"use client"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Gem, Sword, User2, Loader2 } from "lucide-react"
import { Input } from "@/components/ui/input"
@@ -20,19 +21,21 @@ const roleIcons = {
[ROLES.CIVILIAN]: User2,
} as const
const roleNames = {
[ROLES.DUKE]: "公爵",
[ROLES.KNIGHT]: "骑士",
[ROLES.CIVILIAN]: "平民",
} as const
type RoleWithoutEmperor = Exclude<Role, typeof ROLES.EMPEROR>
export function PromotePanel() {
const t = useTranslations("profile.promote")
const tCard = useTranslations("profile.card")
const [searchText, setSearchText] = useState("")
const [loading, setLoading] = useState(false)
const [targetRole, setTargetRole] = useState<RoleWithoutEmperor>(ROLES.KNIGHT)
const { toast } = useToast()
const roleNames = {
[ROLES.DUKE]: tCard("roles.DUKE"),
[ROLES.KNIGHT]: tCard("roles.KNIGHT"),
[ROLES.CIVILIAN]: tCard("roles.CIVILIAN"),
} as const
const handleAction = async () => {
if (!searchText) return
@@ -59,8 +62,8 @@ export function PromotePanel() {
if (!data.user) {
toast({
title: "未找到用户",
description: "请确认用户名或邮箱地址是否正确",
title: t("noUsers"),
description: t("searchPlaceholder"),
variant: "destructive"
})
return
@@ -68,8 +71,8 @@ export function PromotePanel() {
if (data.user.role === targetRole) {
toast({
title: `用户已是${roleNames[targetRole]}`,
description: "无需重复设置",
title: t("updateSuccess"),
description: t("updateSuccess"),
})
return
}
@@ -85,18 +88,18 @@ export function PromotePanel() {
if (!promoteRes.ok) {
const error = await promoteRes.json() as { error: string }
throw new Error(error.error || "设置失败")
throw new Error(error.error || t("updateFailed"))
}
toast({
title: "设置成功",
description: `已将用户 ${data.user.username || data.user.email} 设为${roleNames[targetRole]}`,
title: t("updateSuccess"),
description: `${data.user.username || data.user.email} - ${roleNames[targetRole]}`,
})
setSearchText("")
} catch (error) {
toast({
title: "设置失败",
description: error instanceof Error ? error.message : "请稍后重试",
title: t("updateFailed"),
description: error instanceof Error ? error.message : t("updateFailed"),
variant: "destructive"
})
} finally {
@@ -110,7 +113,7 @@ export function PromotePanel() {
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
<div className="flex items-center gap-2 mb-6">
<Icon className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold"></h2>
<h2 className="text-lg font-semibold">{t("title")}</h2>
</div>
<div className="space-y-4">
@@ -119,7 +122,7 @@ export function PromotePanel() {
<Input
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="输入用户名或邮箱"
placeholder={t("searchPlaceholder")}
/>
</div>
<Select value={targetRole} onValueChange={(value) => setTargetRole(value as RoleWithoutEmperor)}>
@@ -130,19 +133,19 @@ export function PromotePanel() {
<SelectItem value={ROLES.DUKE}>
<div className="flex items-center gap-2">
<Gem className="w-4 h-4" />
{roleNames[ROLES.DUKE]}
</div>
</SelectItem>
<SelectItem value={ROLES.KNIGHT}>
<div className="flex items-center gap-2">
<Sword className="w-4 h-4" />
{roleNames[ROLES.KNIGHT]}
</div>
</SelectItem>
<SelectItem value={ROLES.CIVILIAN}>
<div className="flex items-center gap-2">
<User2 className="w-4 h-4" />
{roleNames[ROLES.CIVILIAN]}
</div>
</SelectItem>
</SelectContent>
@@ -157,7 +160,7 @@ export function PromotePanel() {
{loading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
`设为${roleNames[targetRole]}`
`${t("promote")} ${roleNames[targetRole]}`
)}
</Button>
</div>