From 63f3310fa027cdcba67b0eb339989993437f3494 Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 11 Aug 2026 15:31:13 +0800 Subject: [PATCH] feat(webui): move pages under [locale], compose intl + auth proxy Co-Authored-By: Claude Opus 4.7 --- src/app/[locale]/[...rest]/page.tsx | 9 +++++ src/app/{ => [locale]}/layout.tsx | 50 +++++++++++++++++------- src/app/{ => [locale]}/login/page.tsx | 5 ++- src/app/[locale]/not-found.tsx | 10 +++++ src/app/{ => [locale]}/page.tsx | 3 +- src/lib/authRedirect.test.ts | 8 ++++ src/lib/authRedirect.ts | 8 +++- src/lib/localePath.test.ts | 56 +++++++++++++++++++++++++++ src/lib/localePath.ts | 25 ++++++++++++ src/proxy.ts | 39 ++++++++++++++++--- 10 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 src/app/[locale]/[...rest]/page.tsx rename src/app/{ => [locale]}/layout.tsx (63%) rename src/app/{ => [locale]}/login/page.tsx (97%) create mode 100644 src/app/[locale]/not-found.tsx rename src/app/{ => [locale]}/page.tsx (99%) create mode 100644 src/lib/localePath.test.ts create mode 100644 src/lib/localePath.ts diff --git a/src/app/[locale]/[...rest]/page.tsx b/src/app/[locale]/[...rest]/page.tsx new file mode 100644 index 0000000..8382a73 --- /dev/null +++ b/src/app/[locale]/[...rest]/page.tsx @@ -0,0 +1,9 @@ +import { notFound } from "next/navigation"; + +// Unmatched URLs inside a matched [locale] segment ("/en/typo") bubble to the +// root _not-found otherwise — Next only renders a segment's not-found.tsx on +// a programmatic notFound() call. This catch-all throws it so the translated +// [locale]/not-found.tsx handles unknown paths (next-intl recommended pattern). +export default function CatchAllPage() { + notFound(); +} diff --git a/src/app/layout.tsx b/src/app/[locale]/layout.tsx similarity index 63% rename from src/app/layout.tsx rename to src/app/[locale]/layout.tsx index 15644ae..5b44a26 100644 --- a/src/app/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,25 +1,37 @@ import type { Metadata, Viewport } from "next"; +import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { hasLocale, NextIntlClientProvider } from "next-intl"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { ThemeProvider, ThemedToaster } from "@/providers/ThemeProvider"; -import { AuthRedirectInstaller } from "./components/AuthRedirectInstaller"; +import { AuthRedirectInstaller } from "../components/AuthRedirectInstaller"; import { THEME_STORAGE_KEY } from "@/lib/theme"; +import { routing } from "@/i18n/routing"; import "katex/dist/katex.min.css"; import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"; import localFont from "next/font/local"; -import "./globals.css"; +import "../globals.css"; export const dynamic = "force-dynamic"; -export async function generateMetadata(): Promise { +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale: rawLocale } = await params; const { getSystemConfig, brandingVersion } = await import( "@/lib/server/systemConfig" ); + const locale = hasLocale(routing.locales, rawLocale) + ? rawLocale + : routing.defaultLocale; + const t = await getTranslations({ locale, namespace: "common" }); const config = getSystemConfig(); const faviconVersion = brandingVersion("favicon"); return { title: `${config.branding.wordmark} WebUI`, - description: - "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.", + description: t("metadataDescription"), icons: faviconVersion > 0 ? { icon: `/api/system/branding/asset/favicon?v=${faviconVersion}` } @@ -38,7 +50,7 @@ export const viewport: Viewport = { // Self-hosted Inter variable font; exposes --font-inter, consumed by globals.css. // No CJK glyphs — Chinese text falls through to PingFang/YaHei in the stack. const inter = localFont({ - src: "./fonts/InterVariable.woff2", + src: "../fonts/InterVariable.woff2", variable: "--font-inter", display: "swap", }); @@ -50,14 +62,20 @@ const themeScript = `(function(){var k=${JSON.stringify( THEME_STORAGE_KEY )};var t="system";try{t=localStorage.getItem(k)||"system";}catch(_){}var d=t==="dark"||(t!=="light"&&window.matchMedia("(prefers-color-scheme: dark)").matches);var e=document.documentElement;e.classList.toggle("dark",d);e.style.colorScheme=d?"dark":"light";})();`; -export default function RootLayout({ +export default async function RootLayout({ children, + params, }: { children: React.ReactNode; + params: Promise<{ locale: string }>; }) { + const { locale } = await params; + if (!hasLocale(routing.locales, locale)) { + notFound(); + } return (