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:
@@ -2,6 +2,8 @@ import "./globals.css";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import type { ReactNode } from "react";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
|
||||
import { Toaster } from "@/shared/components/ui/sonner";
|
||||
|
||||
@@ -31,24 +33,35 @@ export const viewport: Viewport = {
|
||||
};
|
||||
|
||||
/**
|
||||
* RootLayout
|
||||
* RootLayout(P1-4:next-intl 接入)
|
||||
*
|
||||
* 仅负责 <html>/<body> 与字体变量 + 全局 Toaster。
|
||||
* 业务 Providers(Apollo/Auth/ThemeI18n)在 ClientShell 中挂载(spec §5.5)。
|
||||
* 职责:
|
||||
* - <html lang={locale}>:从 next-intl getLocale() 获取(cookie 驱动)
|
||||
* - NextIntlClientProvider:将 messages 注入客户端,useTranslations 全局可用
|
||||
* - 字体变量 + 全局 Toaster
|
||||
*
|
||||
* suppressHydrationWarning:ThemeI18nProvider 在客户端切换 .dark class,
|
||||
* 业务 Providers(Apollo/Auth/Theme)在 ClientShell 中挂载(spec §5.5)。
|
||||
*
|
||||
* suppressHydrationWarning:ThemeProvider 在客户端切换 .dark class,
|
||||
* 与 SSR 输出的 <html class=""> 不一致,需抑制 hydration 警告。
|
||||
*
|
||||
* 关联:portal-shell ARCHITECTURE.md §3.4 V3-A6、§8.7
|
||||
*/
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
}): Promise<ReactNode> {
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning className={inter.variable}>
|
||||
<html lang={locale} suppressHydrationWarning className={inter.variable}>
|
||||
<body className="font-sans antialiased">
|
||||
{children}
|
||||
<Toaster />
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,65 +1,68 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { PageHeader } from "@/shared/components/ui/page-header";
|
||||
|
||||
/**
|
||||
* 模板预览首页(ARCHITECTURE.md §10 P1-3)
|
||||
* 模板预览首页(ARCHITECTURE.md §10 P1-3 + P1-4 next-intl)
|
||||
*
|
||||
* /shell/dev/templates — 仅 dev 可见
|
||||
*
|
||||
* 生产环境调用 notFound() 渲染 404,避免模板示例暴露到线上。
|
||||
* middleware 已放行 /shell/dev/**(仅校验登录),此文件做二次守卫。
|
||||
*
|
||||
* P1-4:文案走 next-intl getTranslations(Server Component),
|
||||
* 切换 NEXT_LOCALE cookie 后页面文案即时切换。
|
||||
*
|
||||
* 注:原 ARCHITECTURE.md 使用 `_dev` 命名,但 Next.js 将下划线开头的文件夹
|
||||
* 视为私有文件夹(不参与路由),故改用 `dev` 命名。
|
||||
*/
|
||||
export default function TemplatesIndexPage(): React.ReactElement {
|
||||
export default async function TemplatesIndexPage(): Promise<React.ReactElement> {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getTranslations("shell.dev.templates");
|
||||
|
||||
const templates = [
|
||||
{
|
||||
href: "/shell/dev/templates/list",
|
||||
title: "列表页模板",
|
||||
description: "PageHeader + FilterBar + DataTable + Pagination + 三态",
|
||||
title: t("list"),
|
||||
description: t("listDescription"),
|
||||
},
|
||||
{
|
||||
href: "/shell/dev/templates/detail",
|
||||
title: "详情页模板",
|
||||
description: "PageHeader + 信息区 + Tabs/分区 + 关联列表",
|
||||
title: t("detail"),
|
||||
description: t("detailDescription"),
|
||||
},
|
||||
{
|
||||
href: "/shell/dev/templates/form",
|
||||
title: "表单页模板",
|
||||
description: "PageHeader + 表单 + 提交/取消 + 错误摘要",
|
||||
title: t("form"),
|
||||
description: t("formDescription"),
|
||||
},
|
||||
{
|
||||
href: "/shell/dev/templates/workbench",
|
||||
title: "工作台页模板",
|
||||
description: "三栏(树/画布/属性)复合组件",
|
||||
title: t("workbench"),
|
||||
description: t("workbenchDescription"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<PageHeader
|
||||
title="页面模板四件套"
|
||||
description="P1-3 验收用示例页(仅 dev 可见,生产环境 404)"
|
||||
/>
|
||||
<PageHeader title={t("title")} description={t("description")} />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{templates.map((t) => (
|
||||
{templates.map((tpl) => (
|
||||
<Link
|
||||
key={t.href}
|
||||
href={t.href}
|
||||
key={tpl.href}
|
||||
href={tpl.href}
|
||||
className="block rounded-xl border bg-card p-6 transition-colors hover:bg-accent"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">{t.title}</h2>
|
||||
<h2 className="text-lg font-semibold">{tpl.title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t.description}
|
||||
{tpl.description}
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-muted-foreground">{t.href}</p>
|
||||
<p className="mt-3 text-xs text-muted-foreground">{tpl.href}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user