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 (