feat(portal-shell): integrate next-intl + merge messages from teacher-portal (P1-4)

- Add next-intl v4.13.2 with cookie-based locale (no i18n routing)
- Create src/i18n/request.ts reading NEXT_LOCALE cookie
- Merge zh-CN/en messages from teacher-portal + add shell.dev.templates namespace
- Wrap next.config.js with withNextIntl plugin (Turbopack resolveAlias)
- Refactor RootLayout to async + NextIntlClientProvider + getLocale/getMessages
- Replace ThemeI18nProvider with ThemeProvider (theme-only, i18n removed)
- Remove locale/setLocale from PluginStore
- Rework locale-switcher to useLocale/useTranslations + router.refresh
- Update dev/templates page to use getTranslations (Server Component)
- Fix WorkbenchPageShell test (loading prop + center instead of children)

Verified: locale switch via NEXT_LOCALE cookie changes <html lang> and messages
zh-CN: 保存/取消/切换侧栏 | en: Save/Cancel/Toggle sidebar
typecheck 0 errors | lint 0 errors 2 warnings (generated) | vitest 231 passed
This commit is contained in:
SpecialX
2026-07-22 13:40:44 +08:00
parent 994441c2dc
commit da05c9107a
17 changed files with 1040 additions and 158 deletions

View File

@@ -0,0 +1,32 @@
/**
* next-intl 请求配置App Router 无 i18n 路由模式)
*
* ARCHITECTURE.md §3.4 V3-A6next-intl 正式接入
*
* 采用"无 i18n 路由"策略locale 不出现在 URL 中):
* - locale 由 NEXT_LOCALE cookie 决定locale-switcher 写入)
* - 缺省 locale = zh-CN
* - 切换 locale 时设置 cookie + router.refresh() 触发 RSC 重新渲染
*
* 关联portal-shell ARCHITECTURE.md §3.4 V3-A6、§8.7、§11.2
*/
import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";
export const locales = ["zh-CN", "en"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "zh-CN";
export default getRequestConfig(async () => {
const cookieStore = await cookies();
const cookieLocale = cookieStore.get("NEXT_LOCALE")?.value;
const locale: Locale =
cookieLocale && locales.includes(cookieLocale as Locale)
? (cookieLocale as Locale)
: defaultLocale;
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});