From 63707fa434c37b61da7c8c0265553d1563d1e66b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 17:22:58 +0000
Subject: docs: redesign home page with launcher showcase and fix routing

Co-authored-by: HsiangNianian <44714368+HsiangNianian@users.noreply.github.com>
---
 packages/docs/app/routes/docs.tsx | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 packages/docs/app/routes/docs.tsx

(limited to 'packages/docs/app/routes/docs.tsx')

diff --git a/packages/docs/app/routes/docs.tsx b/packages/docs/app/routes/docs.tsx
new file mode 100644
index 0000000..a1c1707
--- /dev/null
+++ b/packages/docs/app/routes/docs.tsx
@@ -0,0 +1,6 @@
+import type { Route } from './+types/docs';
+import { redirect } from 'react-router';
+
+export function loader({}: Route.LoaderArgs) {
+  return redirect('/docs/en');
+}
-- 
cgit v1.2.3-70-g09d2


From b27777b625fb348f35e435276842668e16456967 Mon Sep 17 00:00:00 2001
From: NtskwK <natsukawa247@outlook.com>
Date: Tue, 3 Feb 2026 11:19:09 +0800
Subject: feat(i18n): add multi-language support for docs

---
 packages/docs/app/docs/page.tsx         |  51 +++++----
 packages/docs/app/docs/search.ts        |   7 +-
 packages/docs/app/lib/i18n.ts           |   8 ++
 packages/docs/app/lib/layout.shared.tsx |  17 ++-
 packages/docs/app/lib/source.ts         |   5 +
 packages/docs/app/root.tsx              |  20 +++-
 packages/docs/app/routes.ts             |  19 +++-
 packages/docs/app/routes/docs.tsx       |  14 ++-
 packages/docs/app/routes/home.tsx       | 196 +++++++++++++++++++-------------
 packages/docs/source.config.ts          |   3 +-
 10 files changed, 222 insertions(+), 118 deletions(-)
 create mode 100644 packages/docs/app/lib/i18n.ts

(limited to 'packages/docs/app/routes/docs.tsx')

diff --git a/packages/docs/app/docs/page.tsx b/packages/docs/app/docs/page.tsx
index 2618989..a9e3433 100644
--- a/packages/docs/app/docs/page.tsx
+++ b/packages/docs/app/docs/page.tsx
@@ -1,51 +1,58 @@
 import type { Route } from './+types/page';
+import defaultMdxComponents from 'fumadocs-ui/mdx';
 import { DocsLayout } from 'fumadocs-ui/layouts/docs';
-import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
+import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/page';
+import { Card, Cards } from 'fumadocs-ui/components/card';
 import { source } from '@/lib/source';
-import defaultMdxComponents from 'fumadocs-ui/mdx';
-import browserCollections from 'fumadocs-mdx:collections/browser';
+import { i18n } from '@/lib/i18n';
 import { baseOptions } from '@/lib/layout.shared';
 import { useFumadocsLoader } from 'fumadocs-core/source/client';
+import browserCollections from 'fumadocs-mdx:collections/browser';
 
 export async function loader({ params }: Route.LoaderArgs) {
-  const slugs = params['*'].split('/').filter((v) => v.length > 0);
-  const page = source.getPage(slugs);
-  if (!page) throw new Response('Not found', { status: 404 });
+  // 从路由参数获取语言，如果没有则使用默认语言
+  // URL 格式: /docs/getting-started (默认语言 zh)
+  // URL 格式: /en/docs/getting-started (英语)
+  const lang = (params.lang && i18n.languages.includes(params.lang as any))
+    ? (params.lang as 'zh' | 'en')
+    : (i18n.defaultLanguage as 'zh' | 'en');
+  
+  // 获取文档路径 slugs
+  const slugs = params['*']?.split('/').filter((v) => v.length > 0) || [];
+  
+  const page = source.getPage(slugs, lang);
+  
+  if (!page) {
+    throw new Response('Not found', { status: 404 });
+  }
 
   return {
     path: page.path,
-    pageTree: await source.serializePageTree(source.getPageTree()),
+    pageTree: await source.serializePageTree(source.getPageTree(lang)),
+    lang,
   };
 }
 
 const clientLoader = browserCollections.docs.createClientLoader({
-  component(
-    { toc, frontmatter, default: Mdx },
-    // you can define props for the `<Content />` component
-    props?: {
-      className?: string;
-    },
-  ) {
+  component({ toc, frontmatter, default: Mdx }) {
     return (
-      <DocsPage toc={toc} {...props}>
-        <title>{frontmatter.title}</title>
-        <meta name="description" content={frontmatter.description} />
+      <DocsPage toc={toc}>
         <DocsTitle>{frontmatter.title}</DocsTitle>
         <DocsDescription>{frontmatter.description}</DocsDescription>
         <DocsBody>
-          <Mdx components={{ ...defaultMdxComponents }} />
+          <Mdx components={{ ...defaultMdxComponents, Card, Cards }} />
         </DocsBody>
       </DocsPage>
     );
   },
 });
 
-export default function Page({ loaderData }: Route.ComponentProps) {
-  const { path, pageTree } = useFumadocsLoader(loaderData);
+export default function Page({ loaderData, params }: Route.ComponentProps) {
+  const { pageTree, lang } = useFumadocsLoader(loaderData);
 
   return (
-    <DocsLayout {...baseOptions()} tree={pageTree}>
-      {clientLoader.useContent(path)}
+    <DocsLayout {...baseOptions(lang)} tree={pageTree}>
+      {clientLoader.useContent(loaderData.path)}
     </DocsLayout>
   );
 }
diff --git a/packages/docs/app/docs/search.ts b/packages/docs/app/docs/search.ts
index 9603c72..a98edd5 100644
--- a/packages/docs/app/docs/search.ts
+++ b/packages/docs/app/docs/search.ts
@@ -3,8 +3,11 @@ import { createFromSource } from 'fumadocs-core/search/server';
 import { source } from '@/lib/source';
 
 const server = createFromSource(source, {
-  // https://docs.orama.com/docs/orama-js/supported-languages
-  language: 'english',
+  localeMap: {
+    zh: {
+      language: 'english',
+    },
+  },
 });
 
 export async function loader({ request }: Route.LoaderArgs) {
diff --git a/packages/docs/app/lib/i18n.ts b/packages/docs/app/lib/i18n.ts
new file mode 100644
index 0000000..a9f18b1
--- /dev/null
+++ b/packages/docs/app/lib/i18n.ts
@@ -0,0 +1,8 @@
+import { defineI18n } from 'fumadocs-core/i18n';
+
+export const i18n = defineI18n({
+  defaultLanguage: 'zh',
+  languages: ['zh', 'en'],
+  hideLocale: 'default-locale',
+  parser: 'dir', // 使用目录结构 (content/zh/*, content/en/*)
+});
diff --git a/packages/docs/app/lib/layout.shared.tsx b/packages/docs/app/lib/layout.shared.tsx
index 6c7a35d..6e90ba0 100644
--- a/packages/docs/app/lib/layout.shared.tsx
+++ b/packages/docs/app/lib/layout.shared.tsx
@@ -1,9 +1,24 @@
 import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
+import { i18n } from './i18n';
 
-export function baseOptions(): BaseLayoutProps {
+export function baseOptions(locale: string): BaseLayoutProps {
+  // 默认语言（zh）不显示前缀，其他语言显示前缀
+  const isDefaultLocale = locale === i18n.defaultLanguage;
+  const localePrefix = isDefaultLocale ? '' : `/${locale}`;
+  
   return {
+    i18n,
     nav: {
       title: 'DropOut',
+      url: localePrefix || '/',
     },
+    githubUrl: 'https://github.com/HydroRoll-Team/DropOut',
+    links: [
+      {
+        type: 'main',
+        text: locale === 'zh' ? '文档' : 'Documentation',
+        url: `${localePrefix}/docs`,
+      },
+    ],
   };
 }
diff --git a/packages/docs/app/lib/source.ts b/packages/docs/app/lib/source.ts
index 97cf767..bce9bf9 100644
--- a/packages/docs/app/lib/source.ts
+++ b/packages/docs/app/lib/source.ts
@@ -1,7 +1,12 @@
 import { loader } from 'fumadocs-core/source';
 import { docs } from 'fumadocs-mdx:collections/server';
+import { i18n } from './i18n';
 
 export const source = loader({
   source: docs.toFumadocsSource(),
   baseUrl: '/docs',
+  i18n,
+  // hideLocale: 'default-locale' 会自动生成正确的 URL:
+  // - 默认语言 (zh): /docs/getting-started
+  // - 其他语言 (en): /en/docs/getting-started
 });
diff --git a/packages/docs/app/root.tsx b/packages/docs/app/root.tsx
index a6c807e..9032c80 100644
--- a/packages/docs/app/root.tsx
+++ b/packages/docs/app/root.tsx
@@ -6,11 +6,25 @@ import {
   Outlet,
   Scripts,
   ScrollRestoration,
+  useParams,
 } from 'react-router';
 import { RootProvider } from 'fumadocs-ui/provider/react-router';
 import type { Route } from './+types/root';
 import './app.css';
+import { defineI18nUI } from 'fumadocs-ui/i18n';
+import { i18n } from './lib/i18n';
 
+const { provider } = defineI18nUI(i18n, {
+  translations: {
+    en: {
+      displayName: 'English',
+    },
+    zh: {
+      displayName: '中文',
+      search: '查找文档',
+    },
+  },
+});
 export const links: Route.LinksFunction = () => [
   { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
   {
@@ -25,8 +39,10 @@ export const links: Route.LinksFunction = () => [
 ];
 
 export function Layout({ children }: { children: React.ReactNode }) {
+  const { lang = i18n.defaultLanguage } = useParams();
+  
   return (
-    <html lang="en" suppressHydrationWarning>
+    <html lang={lang} suppressHydrationWarning>
       <head>
         <meta charSet="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -34,7 +50,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
         <Links />
       </head>
       <body className="flex flex-col min-h-screen">
-        <RootProvider>{children}</RootProvider>
+        <RootProvider i18n={provider(lang)}>{children}</RootProvider>
         <ScrollRestoration />
         <Scripts />
       </body>
diff --git a/packages/docs/app/routes.ts b/packages/docs/app/routes.ts
index 9acc429..2997ecf 100644
--- a/packages/docs/app/routes.ts
+++ b/packages/docs/app/routes.ts
@@ -1,9 +1,16 @@
-import { index, route, type RouteConfig } from '@react-router/dev/routes';
+import { route, type RouteConfig } from '@react-router/dev/routes';
 
 export default [
-  index('routes/home.tsx'),
-  route('docs', 'routes/docs.tsx'),
-  route('docs/*', 'docs/page.tsx'),
-  route('api/search', 'docs/search.ts'),
-  route('*', 'routes/not-found.tsx'),
+  // Home routes: / and /:lang
+  route(':lang?', 'routes/home.tsx', { id: 'home' }),
+
+  // Docs routes: /docs/* and /:lang/docs/*
+  route(':lang?/docs', 'routes/docs.tsx', { id: 'docs' }),
+  route(':lang?/docs/*', 'docs/page.tsx', { id: 'docs-page' }),
+
+  // API routes
+  route('api/search', 'docs/search.ts', { id: 'api-search' }),
+
+  // Catch-all 404
+  route('*', 'routes/not-found.tsx', { id: 'not-found' }),
 ] satisfies RouteConfig;
diff --git a/packages/docs/app/routes/docs.tsx b/packages/docs/app/routes/docs.tsx
index a1c1707..5154d27 100644
--- a/packages/docs/app/routes/docs.tsx
+++ b/packages/docs/app/routes/docs.tsx
@@ -1,6 +1,16 @@
 import type { Route } from './+types/docs';
 import { redirect } from 'react-router';
 
-export function loader({}: Route.LoaderArgs) {
-  return redirect('/docs/en');
+import { i18n } from '@/lib/i18n';
+
+export function loader({ params }: Route.LoaderArgs) {
+  const lang = params.lang as string | undefined;
+  
+  // 如果没有语言参数或是默认语言，重定向到 /docs/getting-started
+  if (!lang || lang === i18n.defaultLanguage) {
+    return redirect('/docs/getting-started');
+  }
+  
+  // 其他语言重定向到 /:lang/docs/getting-started
+  return redirect(`/${lang}/docs/getting-started`);
 }
diff --git a/packages/docs/app/routes/home.tsx b/packages/docs/app/routes/home.tsx
index 1bd2235..66b5333 100644
--- a/packages/docs/app/routes/home.tsx
+++ b/packages/docs/app/routes/home.tsx
@@ -1,43 +1,117 @@
 import type { Route } from './+types/home';
 import { HomeLayout } from 'fumadocs-ui/layouts/home';
-import { Link } from 'react-router';
 import { baseOptions } from '@/lib/layout.shared';
+import { i18n } from '@/lib/i18n';
 
-export function meta({}: Route.MetaArgs) {
+const texts = {
+  en: {
+    hero: {
+      title: 'DropOut Minecraft Launcher',
+      subtitle: 'Modern. Reproducible. Developer-Grade.',
+      description: 'Built with Tauri v2 and Rust for native performance and minimal resource usage',
+      start: 'Get Started',
+      features: 'Features',
+    },
+    features: {
+      items: [
+        { title: 'High Performance', desc: 'Built with Rust and Tauri for minimal resource usage and fast startup times' },
+        { title: 'Modern UI', desc: 'Clean, distraction-free interface with Svelte 5 and Tailwind CSS 4' },
+        { title: 'Secure Auth', desc: 'Microsoft OAuth 2.0 with device code flow and offline mode support' },
+        { title: 'Mod Loaders', desc: 'Built-in support for Fabric and Forge with automatic version management' },
+        { title: 'Java Management', desc: 'Auto-detection and integrated downloader for Adoptium JDK/JRE' },
+        { title: 'Instance System', desc: 'Isolated game environments with independent configs and mods' },
+      ]
+    },
+    why: {
+      title: 'Why DropOut?',
+      items: [
+        { q: 'Your instance worked yesterday but broke today?', a: '→ DropOut makes it traceable.' },
+        { q: 'Sharing a modpack means zipping gigabytes?', a: '→ DropOut shares exact dependency manifests.' },
+        { q: 'Java, loader, mods, configs drift out of sync?', a: '→ DropOut locks them together.' },
+      ]
+    },
+    cta: {
+      title: 'Ready to get started?',
+      desc: 'Check out the documentation to learn more about DropOut',
+      button: 'Read the Docs',
+    }
+  },
+  zh: {
+    hero: {
+      title: 'DropOut Minecraft 启动器',
+      subtitle: '现代、可复现、开发者级',
+      description: '基于 Tauri v2 和 Rust 构建，拥有原生性能和极低的资源占用',
+      start: '开始使用',
+      features: '功能特性',
+    },
+    features: {
+      items: [
+        { title: '高性能', desc: '使用 Rust 和 Tauri 构建，资源占用最小，启动速度极快' },
+        { title: '现代化界面', desc: '简洁、无干扰的界面，使用 Svelte 5 和 Tailwind CSS 4' },
+        { title: '安全认证', desc: '支持微软 OAuth 2.0 设备代码流和离线模式' },
+        { title: '模组支持', desc: '内置 Fabric 和 Forge 支持，自动管理版本' },
+        { title: 'Java 管理', desc: '自动检测并集成 Adoptium JDK/JRE 下载器' },
+        { title: '实例系统', desc: '独立的游戏环境，独立的配置和模组' },
+      ]
+    },
+    why: {
+      title: '为什么选择 DropOut？',
+      items: [
+        { q: '你的实例昨天还能用，今天就坏了？', a: '→ DropOut 让它可追溯。' },
+        { q: '分享模组包意味着打包数GB的文件？', a: '→ DropOut 分享精确的依赖清单。' },
+        { q: 'Java、加载器、模组、配置不同步？', a: '→ DropOut 将它们锁定在一起。' },
+      ]
+    },
+    cta: {
+      title: '准备好开始了？',
+      desc: '查看文档以了解更多关于 DropOut 的信息',
+      button: '阅读文档',
+    }
+  }
+};
+
+export function meta({ params }: Route.MetaArgs) {
   return [
     { title: 'DropOut - Modern Minecraft Launcher' },
     { name: 'description', content: 'A modern, reproducible, and developer-grade Minecraft launcher built with Tauri v2 and Rust.' },
   ];
 }
 
-export default function Home() {
+export default function Home({ params }: Route.ComponentProps) {
+  const lang = (params.lang as 'en' | 'zh') || i18n.defaultLanguage;
+  const t = texts[lang];
+  
+  // 默认语言（zh）不显示前缀，其他语言显示前缀
+  const isDefaultLocale = lang === i18n.defaultLanguage;
+  const localePrefix = isDefaultLocale ? '' : `/${lang}`;
+
   return (
-    <HomeLayout {...baseOptions()}>
+    <HomeLayout {...baseOptions(lang)}>
       <div className="container max-w-6xl mx-auto px-4 py-16">
         {/* Hero Section */}
         <div className="text-center mb-16">
           <h1 className="text-5xl font-bold mb-6 bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
-            DropOut Minecraft Launcher
+            {t.hero.title}
           </h1>
           <p className="text-xl text-fd-muted-foreground mb-2">
-            Modern. Reproducible. Developer-Grade.
+            {t.hero.subtitle}
           </p>
           <p className="text-lg text-fd-muted-foreground max-w-2xl mx-auto mb-8">
-            Built with Tauri v2 and Rust for native performance and minimal resource usage
+            {t.hero.description}
           </p>
           <div className="flex gap-4 justify-center mb-12">
-            <Link
-              className="bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg px-6 py-3 transition-colors"
-              to="/docs/en"
+            <a
+              className="bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg px-6 py-3 transition-colors cursor-pointer"
+              href={`${localePrefix}/docs`}
             >
-              Get Started
-            </Link>
-            <Link
-              className="bg-fd-secondary hover:bg-fd-secondary/80 text-fd-secondary-foreground font-semibold rounded-lg px-6 py-3 transition-colors"
-              to="/docs/en/features"
+              {t.hero.start}
+            </a>
+            <a
+              className="bg-fd-secondary hover:bg-fd-secondary/80 text-fd-secondary-foreground font-semibold rounded-lg px-6 py-3 transition-colors cursor-pointer"
+              href={`${localePrefix}/docs/features`}
             >
-              Features
-            </Link>
+               {t.hero.features}
+            </a>
           </div>
         </div>
 
@@ -54,84 +128,44 @@ export default function Home() {
 
         {/* Features Grid */}
         <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">High Performance</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Built with Rust and Tauri for minimal resource usage and fast startup times
-            </p>
-          </div>
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">Modern UI</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Clean, distraction-free interface with Svelte 5 and Tailwind CSS 4
-            </p>
-          </div>
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">Secure Auth</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Microsoft OAuth 2.0 with device code flow and offline mode support
-            </p>
-          </div>
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">Mod Loaders</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Built-in support for Fabric and Forge with automatic version management
-            </p>
-          </div>
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">Java Management</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Auto-detection and integrated downloader for Adoptium JDK/JRE
-            </p>
-          </div>
-          <div className="p-6 rounded-lg border border-fd-border bg-fd-card">
-            <h3 className="font-semibold text-lg mb-2">Instance System</h3>
-            <p className="text-sm text-fd-muted-foreground">
-              Isolated game environments with independent configs and mods
-            </p>
-          </div>
+          {t.features.items.map((item, i) => (
+             <div key={i} className="p-6 rounded-lg border border-fd-border bg-fd-card">
+              <h3 className="font-semibold text-lg mb-2">{item.title}</h3>
+              <p className="text-sm text-fd-muted-foreground">
+                {item.desc}
+              </p>
+            </div>
+          ))}
         </div>
 
         {/* Why DropOut Section */}
         <div className="text-center mb-16">
-          <h2 className="text-3xl font-bold mb-6">Why DropOut?</h2>
+          <h2 className="text-3xl font-bold mb-6">{t.why.title}</h2>
           <div className="max-w-3xl mx-auto space-y-4 text-left">
-            <div className="p-4 rounded-lg bg-fd-muted/50">
-              <p className="text-fd-foreground">
-                <span className="font-semibold">Your instance worked yesterday but broke today?</span>
-                <br />
-                <span className="text-fd-muted-foreground">→ DropOut makes it traceable.</span>
-              </p>
-            </div>
-            <div className="p-4 rounded-lg bg-fd-muted/50">
-              <p className="text-fd-foreground">
-                <span className="font-semibold">Sharing a modpack means zipping gigabytes?</span>
-                <br />
-                <span className="text-fd-muted-foreground">→ DropOut shares exact dependency manifests.</span>
-              </p>
-            </div>
-            <div className="p-4 rounded-lg bg-fd-muted/50">
-              <p className="text-fd-foreground">
-                <span className="font-semibold">Java, loader, mods, configs drift out of sync?</span>
-                <br />
-                <span className="text-fd-muted-foreground">→ DropOut locks them together.</span>
-              </p>
-            </div>
+            {t.why.items.map((item, i) => (
+              <div key={i} className="p-4 rounded-lg bg-fd-muted/50">
+                <p className="text-fd-foreground">
+                  <span className="font-semibold">{item.q}</span>
+                  <br />
+                  <span className="text-fd-muted-foreground">{item.a}</span>
+                </p>
+              </div>
+            ))}
           </div>
         </div>
 
         {/* CTA Section */}
         <div className="text-center py-12 px-6 rounded-xl bg-gradient-to-r from-blue-600/10 to-cyan-500/10 border border-blue-600/20">
-          <h2 className="text-3xl font-bold mb-4">Ready to get started?</h2>
+          <h2 className="text-3xl font-bold mb-4">{t.cta.title}</h2>
           <p className="text-lg text-fd-muted-foreground mb-6">
-            Check out the documentation to learn more about DropOut
+            {t.cta.desc}
           </p>
-          <Link
+          <a
             className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg px-8 py-3 transition-colors"
-            to="/docs/en/getting-started"
+            href={`${localePrefix}/docs/getting-started`}
           >
-            Read the Docs
-          </Link>
+            {t.cta.button}
+          </a>
         </div>
       </div>
     </HomeLayout>
diff --git a/packages/docs/source.config.ts b/packages/docs/source.config.ts
index 1e8ac56..d67a91b 100644
--- a/packages/docs/source.config.ts
+++ b/packages/docs/source.config.ts
@@ -1,8 +1,7 @@
 import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
 
 export const docs = defineDocs({
-  dir: 'content/docs',
-  i18n: true,
+  dir: 'content',
 });
 
 export default defineConfig();
-- 
cgit v1.2.3-70-g09d2

