mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-07 01:54:24 +08:00
feat(i18n): add multilingual support for documentation with Chinese and English content
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
return <HomeLayout {...baseOptions(lang)}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react';
|
||||
import { appName, gitConfig } from '@/lib/shared';
|
||||
import { appNames, gitConfig } from '@/lib/shared';
|
||||
import { localizePath, type Locale } from '@/lib/i18n';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
||||
const demoUrl = 'https://canvas.best/';
|
||||
@@ -11,45 +13,79 @@ const darkStarHistoryChart = `${starHistoryChart}&theme=dark`;
|
||||
const previewImages = [
|
||||
{
|
||||
src: 'https://i.ibb.co/TDFvGWDT/image.png',
|
||||
title: '画布编排',
|
||||
title: { en: 'Canvas composition', 'zh-CN': '画布编排' },
|
||||
},
|
||||
{
|
||||
src: 'https://i.ibb.co/zVwJq3YS/image.png',
|
||||
title: '图片生成',
|
||||
title: { en: 'Image generation', 'zh-CN': '图片生成' },
|
||||
},
|
||||
{
|
||||
src: 'https://i.ibb.co/PvY3qhhK/image.png',
|
||||
title: '参考图编辑',
|
||||
title: { en: 'Reference editing', 'zh-CN': '参考图编辑' },
|
||||
},
|
||||
{
|
||||
src: 'https://i.ibb.co/7D04LwN/image.png',
|
||||
title: '节点工作流',
|
||||
title: { en: 'Node workflow', 'zh-CN': '节点工作流' },
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const messages = {
|
||||
en: {
|
||||
eyebrow: 'Open-source AI image creation workspace',
|
||||
center: 'Documentation',
|
||||
description: 'An infinite canvas for image creation that brings canvas composition, AI generation, reference editing, prompt libraries, and reusable assets into one workflow.',
|
||||
quickStart: 'Quick Start',
|
||||
demo: 'Live Demo',
|
||||
gallery: 'Gallery',
|
||||
features: 'Explore Features',
|
||||
previewAlt: 'Infinite Canvas preview',
|
||||
contributors: 'Contributors',
|
||||
contributorsDescription: 'Thank you to everyone who has contributed to this project',
|
||||
contributorsAlt: 'Contributor avatars',
|
||||
},
|
||||
'zh-CN': {
|
||||
eyebrow: '开源 AI 图片创作工作台',
|
||||
center: '文档中心',
|
||||
description: '面向图片创作的无限画布,把画布编排、AI 生成、参考图编辑、提示词库和素材沉淀放在同一个工作流里。',
|
||||
quickStart: '快速开始',
|
||||
demo: '在线体验',
|
||||
gallery: '效果展示',
|
||||
features: '功能介绍',
|
||||
previewAlt: '无限画布效果图',
|
||||
contributors: '开发贡献者',
|
||||
contributorsDescription: '感谢所有为本项目做出贡献的开发者',
|
||||
contributorsAlt: '开发贡献者头像',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function HomePage({ params }: PageProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const locale = lang as Locale;
|
||||
const text = messages[locale];
|
||||
const appName = appNames[locale];
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-5 pb-16 pt-8 md:px-10 md:pt-14">
|
||||
<section className="grid min-h-[520px] items-center gap-10 border-b border-zinc-200 pb-12 dark:border-zinc-800 lg:grid-cols-[0.88fr_1.12fr]">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
<Rocket className="size-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||
开源 AI 图片创作工作台
|
||||
{text.eyebrow}
|
||||
</div>
|
||||
<h1 className="mt-6 max-w-3xl text-4xl font-semibold leading-tight text-zinc-950 dark:text-zinc-50 md:text-6xl [font-family:var(--font-display)]">
|
||||
{appName}
|
||||
<span className="block text-zinc-500 dark:text-zinc-400">文档中心</span>
|
||||
<span className="block text-zinc-500 dark:text-zinc-400">{text.center}</span>
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-base leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
面向图片创作的无限画布,把画布编排、AI 生成、参考图编辑、提示词库和素材沉淀放在同一个工作流里。
|
||||
{text.description}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/docs/overview/quick-start"
|
||||
href={localizePath(locale, '/docs/overview/quick-start')}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-zinc-950 px-5 py-3 text-sm font-medium text-white transition hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-950 dark:hover:bg-zinc-200"
|
||||
>
|
||||
<BookOpen className="size-4" />
|
||||
快速开始
|
||||
{text.quickStart}
|
||||
</Link>
|
||||
<a
|
||||
href={githubUrl}
|
||||
@@ -66,7 +102,7 @@ export default function HomePage() {
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full border border-zinc-300 px-5 py-3 text-sm font-medium text-zinc-900 transition hover:border-zinc-900 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-500 dark:hover:bg-zinc-900"
|
||||
>
|
||||
在线体验
|
||||
{text.demo}
|
||||
<ArrowUpRight className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -75,7 +111,7 @@ export default function HomePage() {
|
||||
<div className="overflow-hidden rounded-2xl lg:w-[108%] lg:max-w-none">
|
||||
<img
|
||||
src={previewImages[3].src}
|
||||
alt="无限画布效果图"
|
||||
alt={text.previewAlt}
|
||||
className="aspect-[16/10] w-full rounded-xl object-cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -85,14 +121,14 @@ export default function HomePage() {
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
|
||||
效果展示
|
||||
{text.gallery}
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/docs/overview/features"
|
||||
href={localizePath(locale, '/docs/overview/features')}
|
||||
className="inline-flex w-fit items-center gap-1.5 text-sm font-medium text-zinc-800 transition hover:text-zinc-950 dark:text-zinc-200 dark:hover:text-white"
|
||||
>
|
||||
功能介绍
|
||||
{text.features}
|
||||
<ArrowUpRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -101,7 +137,7 @@ export default function HomePage() {
|
||||
<img
|
||||
key={item.src}
|
||||
src={item.src}
|
||||
alt={`${item.title}效果图`}
|
||||
alt={item.title[locale]}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="aspect-[16/10] w-full rounded-2xl object-cover"
|
||||
@@ -112,10 +148,10 @@ export default function HomePage() {
|
||||
|
||||
<section className="mx-auto mt-16 w-full max-w-4xl text-center">
|
||||
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
|
||||
开发贡献者
|
||||
{text.contributors}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
感谢所有为本项目做出贡献的开发者
|
||||
{text.contributorsDescription}
|
||||
</p>
|
||||
<div className="mt-7 flex justify-center">
|
||||
<a
|
||||
@@ -126,7 +162,7 @@ export default function HomePage() {
|
||||
>
|
||||
<img
|
||||
src={`https://contrib.rocks/image?repo=${gitConfig.user}/${gitConfig.repo}`}
|
||||
alt="开发贡献者头像"
|
||||
alt={text.contributorsAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="max-w-full"
|
||||
@@ -169,3 +205,20 @@ export default function HomePage() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps<'/[lang]'>): Promise<Metadata> {
|
||||
const { lang } = await params;
|
||||
const locale = lang as Locale;
|
||||
const text = messages[locale];
|
||||
|
||||
return {
|
||||
title: `${appNames[locale]} ${text.center}`,
|
||||
description: text.description,
|
||||
alternates: {
|
||||
languages: {
|
||||
en: '/',
|
||||
'zh-CN': '/zh-CN',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,18 +3,22 @@ import { source } from '@/lib/source';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default async function Page(props: PageProps<'/docs/[...slug]'>) {
|
||||
export default async function Page(props: PageProps<'/[lang]/docs/[...slug]'>) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return <DocPageContent page={page} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/docs/[...slug]'>): Promise<Metadata> {
|
||||
export async function generateMetadata(props: PageProps<'/[lang]/docs/[...slug]'>): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return getDocPageMetadata(page);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { DocsTopTabs } from '@/components/docs-top-tabs';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
|
||||
const { lang } = await params;
|
||||
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()} tabs={false}>
|
||||
<DocsLayout {...baseOptions(lang)} tree={source.getPageTree(lang)} tabs={false}>
|
||||
<DocsTopTabs />
|
||||
{children}
|
||||
</DocsLayout>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { Markdown } from 'fumadocs-core/content/md';
|
||||
import { getTableOfContents } from 'fumadocs-core/content/toc';
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Metadata } from 'next';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
title: 'Infinite Canvas Documentation',
|
||||
description: 'Features, canvas guides, deployment, development, business, and support',
|
||||
index: 'index.md',
|
||||
},
|
||||
'zh-CN': {
|
||||
title: '无限画布文档',
|
||||
description: '功能说明、操作手册、部署方式、开发文档、商务合作与赞助支持',
|
||||
index: 'index.zh-CN.md',
|
||||
},
|
||||
};
|
||||
|
||||
async function readDocsIndex(locale: keyof typeof messages) {
|
||||
return readFile(join(process.cwd(), messages[locale].index), 'utf8');
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps<'/[lang]/docs'>) {
|
||||
const { lang } = await params;
|
||||
const locale = lang as keyof typeof messages;
|
||||
const content = await readDocsIndex(locale);
|
||||
const text = messages[locale];
|
||||
const toc = getTableOfContents(content);
|
||||
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{text.title}</DocsTitle>
|
||||
<DocsDescription>{text.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||
{content}
|
||||
</Markdown>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps<'/[lang]/docs'>): Promise<Metadata> {
|
||||
const { lang } = await params;
|
||||
const text = messages[lang as keyof typeof messages];
|
||||
|
||||
return {
|
||||
title: text.title,
|
||||
description: text.description,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { Markdown } from 'fumadocs-core/content/md';
|
||||
import { getTableOfContents } from 'fumadocs-core/content/toc';
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Metadata } from 'next';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
title: 'Changelog',
|
||||
description: 'Project release history',
|
||||
content: '# Changelog\n\nThe detailed changelog is currently maintained in Chinese. See the [source changelog](https://github.com/basketikun/infinite-canvas/blob/main/CHANGELOG.md) for all releases.',
|
||||
},
|
||||
'zh-CN': {
|
||||
title: '更新日志',
|
||||
description: '项目版本变更记录',
|
||||
},
|
||||
};
|
||||
|
||||
async function readChangelog() {
|
||||
return readFile(join(process.cwd(), '..', 'CHANGELOG.md'), 'utf8');
|
||||
}
|
||||
|
||||
export default async function ChangelogPage({ params }: PageProps<'/[lang]/docs/progress/changelog'>) {
|
||||
const { lang } = await params;
|
||||
const text = messages[lang as keyof typeof messages];
|
||||
const changelog = lang === 'zh-CN' ? await readChangelog() : messages.en.content;
|
||||
const toc = getTableOfContents(changelog);
|
||||
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{text.title}</DocsTitle>
|
||||
<DocsDescription>{text.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||
{changelog}
|
||||
</Markdown>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps<'/[lang]/docs/progress/changelog'>): Promise<Metadata> {
|
||||
const { lang } = await params;
|
||||
const text = messages[lang as keyof typeof messages];
|
||||
|
||||
return {
|
||||
title: text.title,
|
||||
description: text.description,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Provider } from '@/components/provider';
|
||||
import '../global.css';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
|
||||
return (
|
||||
<html lang={lang} suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<Provider locale={lang}>{children}</Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
@@ -5,7 +5,14 @@ import { createFromSource } from 'fumadocs-core/search/server';
|
||||
export const revalidate = false;
|
||||
|
||||
export const { staticGET: GET } = createFromSource(source, {
|
||||
components: {
|
||||
tokenizer: createDocsSearchTokenizer(),
|
||||
localeMap: {
|
||||
en: {
|
||||
language: 'english',
|
||||
},
|
||||
'zh-CN': {
|
||||
components: {
|
||||
tokenizer: createDocsSearchTokenizer(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { Markdown } from 'fumadocs-core/content/md';
|
||||
import { getTableOfContents } from 'fumadocs-core/content/toc';
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Metadata } from 'next';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
|
||||
const title = '无限画布文档';
|
||||
const description = '功能说明、操作手册、部署方式、开发文档、商务合作与赞助支持';
|
||||
|
||||
async function readDocsIndex() {
|
||||
return readFile(join(process.cwd(), 'index.md'), 'utf8');
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const content = await readDocsIndex();
|
||||
const toc = getTableOfContents(content);
|
||||
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{title}</DocsTitle>
|
||||
<DocsDescription>{description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||
{content}
|
||||
</Markdown>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { Markdown } from 'fumadocs-core/content/md';
|
||||
import { getTableOfContents } from 'fumadocs-core/content/toc';
|
||||
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Metadata } from 'next';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
|
||||
const title = '更新日志';
|
||||
const description = '项目版本变更记录';
|
||||
|
||||
async function readChangelog() {
|
||||
return readFile(join(process.cwd(), '..', 'CHANGELOG.md'), 'utf8');
|
||||
}
|
||||
|
||||
export default async function ChangelogPage() {
|
||||
const changelog = await readChangelog();
|
||||
const toc = getTableOfContents(changelog);
|
||||
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{title}</DocsTitle>
|
||||
<DocsDescription>{description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||
{changelog}
|
||||
</Markdown>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Provider } from '@/components/provider';
|
||||
import './global.css';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<Provider>{children}</Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import { join } from 'node:path';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
export async function GET(request: Request) {
|
||||
const locale = new URL(request.url).searchParams.get('locale') ?? 'en';
|
||||
const docsIndex = await readFile(join(process.cwd(), locale === 'zh-CN' ? 'index.zh-CN.md' : 'index.md'), 'utf8');
|
||||
const scan = source.getPages(locale).map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
|
||||
return new Response([docsIndex, ...scanned].join('\n\n'));
|
||||
|
||||
@@ -3,10 +3,11 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
|
||||
export async function GET(req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
|
||||
const { slug } = await params;
|
||||
const locale = new URL(req.url).searchParams.get('locale') ?? 'en';
|
||||
// remove the appended "content.md"
|
||||
const page = source.getPage(slug?.slice(0, -1));
|
||||
const page = source.getPage(slug?.slice(0, -1), locale);
|
||||
if (!page) notFound();
|
||||
|
||||
return new Response(await getLLMText(page), {
|
||||
@@ -17,7 +18,7 @@ export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/doc
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
return source.getPages('en').map((page) => ({
|
||||
slug: getPageMarkdownUrl(page).segments,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { join } from 'node:path';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
|
||||
return new Response([docsIndex, llms(source).index()].join('\n\n'));
|
||||
export async function GET(request: Request) {
|
||||
const locale = new URL(request.url).searchParams.get('locale') ?? 'en';
|
||||
const docsIndex = await readFile(join(process.cwd(), locale === 'zh-CN' ? 'index.zh-CN.md' : 'index.md'), 'utf8');
|
||||
return new Response([docsIndex, llms(source).index(locale)].join('\n\n'));
|
||||
}
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { localizePath, type Locale } from '@/lib/i18n';
|
||||
|
||||
const tabs = [
|
||||
{ title: '项目介绍', href: '/docs/overview/quick-start', prefix: '/docs/overview' },
|
||||
{ title: '操作手册', href: '/docs/canvas/canvas-node-manual', prefix: '/docs/canvas' },
|
||||
{ title: '开发文档', href: '/docs/development/local-development', prefix: '/docs/development' },
|
||||
{ title: '项目进度', href: '/docs/progress/changelog', prefix: '/docs/progress' },
|
||||
{ title: '商务合作', href: '/docs/business/business', prefix: '/docs/business' },
|
||||
{ title: '赞助支持', href: '/docs/support/sponsor', prefix: '/docs/support' },
|
||||
{ title: { en: 'Overview', 'zh-CN': '项目介绍' }, href: '/docs/overview/quick-start', prefix: '/docs/overview' },
|
||||
{ title: { en: 'Canvas Guide', 'zh-CN': '操作手册' }, href: '/docs/canvas/canvas-node-manual', prefix: '/docs/canvas' },
|
||||
{ title: { en: 'Development', 'zh-CN': '开发文档' }, href: '/docs/development/local-development', prefix: '/docs/development' },
|
||||
{ title: { en: 'Progress', 'zh-CN': '项目进度' }, href: '/docs/progress/changelog', prefix: '/docs/progress' },
|
||||
{ title: { en: 'Business', 'zh-CN': '商务合作' }, href: '/docs/business/business', prefix: '/docs/business' },
|
||||
{ title: { en: 'Support', 'zh-CN': '赞助支持' }, href: '/docs/support/sponsor', prefix: '/docs/support' },
|
||||
];
|
||||
|
||||
export function DocsTopTabs() {
|
||||
const pathname = usePathname();
|
||||
const { lang } = useParams<{ lang: Locale }>();
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-30 hidden h-12 self-start overflow-x-auto border-b bg-fd-background/95 px-6 pt-3 backdrop-blur [grid-area:main] md:flex xl:px-8">
|
||||
<div className="flex flex-row items-end gap-6">
|
||||
{tabs.map((tab) => {
|
||||
const active = tab.prefix ? pathname === tab.href || pathname.startsWith(`${tab.prefix}/`) : pathname === tab.href;
|
||||
const href = localizePath(lang, tab.href);
|
||||
const prefix = localizePath(lang, tab.prefix);
|
||||
const active = pathname === href || pathname.startsWith(`${prefix}/`);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
'inline-flex border-b-2 border-transparent pb-1.5 text-sm font-medium text-nowrap text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground',
|
||||
active && 'border-fd-primary text-fd-primary',
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
{tab.title[lang]}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
import SearchDialog from '@/components/search';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { i18nProvider } from 'fumadocs-ui/i18n';
|
||||
import { type ReactNode } from 'react';
|
||||
import { translations } from '@/lib/layout.shared';
|
||||
|
||||
export function Provider({ children }: { children: ReactNode }) {
|
||||
return <RootProvider search={{ SearchDialog }}>{children}</RootProvider>;
|
||||
export function Provider({ locale, children }: { locale: string; children: ReactNode }) {
|
||||
return <RootProvider i18n={i18nProvider(translations, locale)} search={{ SearchDialog }}>{children}</RootProvider>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@ import { create } from '@orama/orama';
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
|
||||
|
||||
function initOrama() {
|
||||
function initOrama(locale?: string) {
|
||||
if (locale !== 'zh-CN') {
|
||||
return create({
|
||||
schema: { _: 'string' },
|
||||
language: 'english',
|
||||
});
|
||||
}
|
||||
|
||||
return create({
|
||||
schema: { _: 'string' },
|
||||
components: {
|
||||
@@ -25,7 +32,7 @@ function initOrama() {
|
||||
}
|
||||
|
||||
export default function DefaultSearchDialog(props: SharedProps) {
|
||||
const { locale } = useI18n(); // (optional) for i18n
|
||||
const { locale } = useI18n();
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: 'static',
|
||||
initOrama,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MarkdownCopyButton,
|
||||
ViewOptionsPopover,
|
||||
} from 'fumadocs-ui/layouts/docs/page';
|
||||
import { localizePath } from './i18n';
|
||||
|
||||
export type DocPageData = (typeof source)['$inferPage'];
|
||||
|
||||
@@ -38,8 +39,16 @@ export function DocPageContent({ page }: { page: DocPageData }) {
|
||||
}
|
||||
|
||||
export function getDocPageMetadata(page: DocPageData): Metadata {
|
||||
const path = `/docs/${page.slugs.join('/')}`;
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
alternates: {
|
||||
languages: {
|
||||
en: localizePath('en', path),
|
||||
'zh-CN': localizePath('zh-CN', path),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export type Locale = 'en' | 'zh-CN';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
defaultLanguage: 'en',
|
||||
languages: ['en', 'zh-CN'],
|
||||
parser: 'dot',
|
||||
hideLocale: 'default-locale',
|
||||
fallbackLanguage: null,
|
||||
});
|
||||
|
||||
export function localizePath(locale: string, path: string) {
|
||||
return locale === i18n.defaultLanguage ? path : `/${locale}${path}`;
|
||||
}
|
||||
@@ -1,11 +1,52 @@
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { appName, gitConfig } from './shared';
|
||||
import { appNames, gitConfig } from './shared';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { i18n } from './i18n';
|
||||
import { uiTranslations } from 'fumadocs-ui/i18n';
|
||||
|
||||
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
||||
const qqUrl = 'https://qm.qq.com/q/DFnKzZ807u';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
export const translations = i18n.translations().extend(uiTranslations()).add('ui', {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
'zh-CN': {
|
||||
displayName: '简体中文',
|
||||
search: '搜索文档',
|
||||
searchNoResult: '没有找到结果',
|
||||
searchOpen: '打开搜索',
|
||||
searchClose: '关闭搜索',
|
||||
toc: '本页目录',
|
||||
tocNoHeadings: '本页没有标题',
|
||||
tocInline: '本页内容',
|
||||
chooseLanguage: '选择语言',
|
||||
nextPage: '下一页',
|
||||
previousPage: '上一页',
|
||||
chooseTheme: '选择主题',
|
||||
themeToggle: '切换主题',
|
||||
themeLight: '浅色',
|
||||
themeDark: '深色',
|
||||
themeSystem: '跟随系统',
|
||||
codeBlockCopy: '复制代码',
|
||||
codeBlockCopied: '已复制',
|
||||
menuToggle: '切换菜单',
|
||||
pageActionsCopyMarkdown: '复制 Markdown',
|
||||
pageActionsOpen: '打开',
|
||||
pageActionsOpenGitHub: '在 GitHub 中打开',
|
||||
pageActionsViewMarkdown: '查看 Markdown',
|
||||
sidebarOpen: '打开侧边栏',
|
||||
sidebarCollapse: '收起侧边栏',
|
||||
notFoundTitle: '页面不存在',
|
||||
notFoundDescription: '你访问的页面不存在。',
|
||||
notFoundLink: '返回首页',
|
||||
},
|
||||
});
|
||||
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
const chinese = locale === 'zh-CN';
|
||||
const appName = appNames[locale as keyof typeof appNames];
|
||||
|
||||
return {
|
||||
nav: {
|
||||
title: (
|
||||
@@ -17,14 +58,14 @@ export function baseOptions(): BaseLayoutProps {
|
||||
},
|
||||
links: [
|
||||
{
|
||||
text: '文档导航',
|
||||
url: '/docs/overview/quick-start',
|
||||
text: chinese ? '文档导航' : 'Documentation',
|
||||
url: `${chinese ? '/zh-CN' : ''}/docs/overview/quick-start`,
|
||||
on: 'nav',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span>在线体验</span>
|
||||
<span>{chinese ? '在线体验' : 'Live Demo'}</span>
|
||||
<ArrowUpRight className="size-4" />
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const appName = '无限画布';
|
||||
export const appNames = {
|
||||
en: 'Infinite Canvas',
|
||||
'zh-CN': '无限画布',
|
||||
};
|
||||
export const docsRoute = '/docs';
|
||||
export const docsContentRoute = '/llms.mdx/docs';
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { docs } from 'collections/server';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { docsContentRoute, docsRoute } from './shared';
|
||||
import { i18n } from './i18n';
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
baseUrl: docsRoute,
|
||||
source: docs.toFumadocsSource(),
|
||||
i18n,
|
||||
plugins: [],
|
||||
});
|
||||
|
||||
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
|
||||
const segments = [...page.slugs, 'content.md'];
|
||||
const locale = page.locale === i18n.defaultLanguage ? '' : `?locale=${page.locale}`;
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `${docsContentRoute}/${segments.join('/')}`,
|
||||
url: `${docsContentRoute}/${segments.join('/')}${locale}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
|
||||
|
||||
export default createI18nMiddleware(i18n);
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
|
||||
};
|
||||
Reference in New Issue
Block a user