aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages
diff options
context:
space:
mode:
authorNtskwK <natsukawa247@outlook.com>2026-02-03 11:19:09 +0800
committerNtskwK <natsukawa247@outlook.com>2026-02-03 11:19:09 +0800
commitb27777b625fb348f35e435276842668e16456967 (patch)
tree8b9b05b075f762a541ffb4e027875051fe9f00e5 /packages
parent9627912c1f7910f786c555a3a9485f0a8c4211bf (diff)
downloadDropOut-b27777b625fb348f35e435276842668e16456967.tar.gz
DropOut-b27777b625fb348f35e435276842668e16456967.zip
feat(i18n): add multi-language support for docs
Diffstat (limited to 'packages')
-rw-r--r--packages/docs/app/docs/page.tsx51
-rw-r--r--packages/docs/app/docs/search.ts7
-rw-r--r--packages/docs/app/lib/i18n.ts8
-rw-r--r--packages/docs/app/lib/layout.shared.tsx17
-rw-r--r--packages/docs/app/lib/source.ts5
-rw-r--r--packages/docs/app/root.tsx20
-rw-r--r--packages/docs/app/routes.ts19
-rw-r--r--packages/docs/app/routes/docs.tsx14
-rw-r--r--packages/docs/app/routes/home.tsx196
-rw-r--r--packages/docs/source.config.ts3
10 files changed, 222 insertions, 118 deletions
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();