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.
This commit is contained in:
32
src/i18n/actions.ts
Normal file
32
src/i18n/actions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
"use server"
|
||||
|
||||
import { cookies } from "next/headers"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import {
|
||||
LOCALE_COOKIE,
|
||||
LOCALE_COOKIE_OPTIONS,
|
||||
type Locale,
|
||||
isLocale,
|
||||
} from "@/shared/i18n/locale"
|
||||
|
||||
/**
|
||||
* 切换语言:写入 cookie + 触发页面刷新。
|
||||
*
|
||||
* 设计说明:
|
||||
* - 不使用 URL 路由段,避免破坏现有路由组结构
|
||||
* - cookie 持久化用户偏好,SSR 时由 i18n/request.ts 读取
|
||||
* - revalidatePath 确保所有页面用新 locale 重新渲染
|
||||
*/
|
||||
export async function setLocaleAction(locale: string): Promise<{ success: boolean }> {
|
||||
if (!isLocale(locale)) {
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set(LOCALE_COOKIE, locale as Locale, LOCALE_COOKIE_OPTIONS)
|
||||
|
||||
// 刷新所有页面,让 SSR 用新 locale 重新渲染
|
||||
revalidatePath("/", "layout")
|
||||
return { success: true }
|
||||
}
|
||||
40
src/i18n/request.ts
Normal file
40
src/i18n/request.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { DEFAULT_LOCALE, LOCALE_COOKIE, isLocale } from "@/shared/i18n/locale";
|
||||
|
||||
/**
|
||||
* next-intl 请求配置(without i18n routing 模式)。
|
||||
*
|
||||
* locale 来源优先级:
|
||||
* 1. cookie `NEXT_LOCALE`(用户主动切换时写入)
|
||||
* 2. 默认值 `zh-CN`
|
||||
*
|
||||
* 不使用 Accept-Language 自动协商,避免 SSR 与客户端 hydration 不一致。
|
||||
* 用户切换语言时通过 setLocaleAction 写入 cookie + router.refresh()。
|
||||
*/
|
||||
export default getRequestConfig(async () => {
|
||||
const cookieStore = await cookies();
|
||||
const cookieValue = cookieStore.get(LOCALE_COOKIE)?.value;
|
||||
const locale = isLocale(cookieValue) ? cookieValue : DEFAULT_LOCALE;
|
||||
|
||||
// 按命名空间拆分加载,避免单文件过大
|
||||
const [common, auth, onboarding, classes, errors] = await Promise.all([
|
||||
import(`@/shared/i18n/messages/${locale}/common.json`),
|
||||
import(`@/shared/i18n/messages/${locale}/auth.json`),
|
||||
import(`@/shared/i18n/messages/${locale}/onboarding.json`),
|
||||
import(`@/shared/i18n/messages/${locale}/classes.json`),
|
||||
import(`@/shared/i18n/messages/${locale}/errors.json`),
|
||||
]);
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: {
|
||||
common: common.default,
|
||||
auth: auth.default,
|
||||
onboarding: onboarding.default,
|
||||
classes: classes.default,
|
||||
errors: errors.default,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user