Files
NextEdu/src/shared/components/locale-switcher.tsx
SpecialX c90748124d feat: introduce i18n system and class invitation codes
Add complete i18n infrastructure using next-intl (cookie-driven, without i18n routing) with zh-CN/en dictionary files, locale switcher, and NextIntlClientProvider in root layout. Add class invitation code system with new class_invitation_codes table, data-access layer (generate/validate/consume/revoke), server actions with permission checks, rate limiting, and audit logging. Add class-invitation-manager UI component. Refactor onboarding stepper to use i18n translations and accept new invitation code format (6-char alphanumeric) with backward compatibility for legacy 6-digit codes.
2026-06-22 14:04:55 +08:00

76 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import * as React from "react"
import { useTransition } from "react"
import { useLocale, useTranslations } from "next-intl"
import { Check, Globe } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/shared/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { LOCALES, type Locale } from "@/shared/i18n/locale"
import { setLocaleAction } from "@/i18n/actions"
import { cn } from "@/shared/lib/utils"
/**
* 语言切换组件v3 i18n 体系)。
*
* 设计:
* - 不使用 URL 路由段,通过 cookie 持久化用户偏好
* - 切换时调用 setLocaleAction 写入 cookie + revalidatePath
* - 用 useTransition 保证切换过程不阻塞 UI
*/
export function LocaleSwitcher({ compact = false }: { compact?: boolean }) {
const locale = useLocale()
const t = useTranslations("common.locale")
const [isPending, startTransition] = useTransition()
function handleSelect(next: Locale) {
if (next === locale) return
startTransition(async () => {
const result = await setLocaleAction(next)
if (result.success) {
toast.success(t("switch"))
} else {
toast.error(t("switch"))
}
})
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size={compact ? "icon" : "sm"}
disabled={isPending}
aria-label={t("switch")}
className={cn(compact && "h-9 w-9")}
>
<Globe className="h-4 w-4" />
{!compact ? (
<span className="ml-1.5 text-sm">{t(locale)}</span>
) : null}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{LOCALES.map((l) => (
<DropdownMenuItem
key={l}
onClick={() => handleSelect(l)}
className="flex items-center justify-between gap-2"
>
<span>{t(l)}</span>
{l === locale ? <Check className="h-4 w-4" /> : null}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}