feat(docs): enhance documentation site with internationalization and layout updates

- Updated the demo API to support locale-based content rendering.
- Refactored the main layout to accommodate language-specific metadata and routing.
- Introduced new components for blog and documentation pages, ensuring proper language handling.
- Added styles for improved UI consistency across different sections.
- Removed deprecated layout files and streamlined the structure for better maintainability.
This commit is contained in:
leookun
2026-08-28 00:35:31 +08:00
parent 6c758f2b44
commit eca063e424
55 changed files with 6700 additions and 407 deletions
+4 -2
View File
@@ -1,6 +1,7 @@
import { loader } from 'fumadocs-core/source';
import { pageSchema } from 'fumadocs-core/source/schema';
import { defineCollections } from 'fumadocs-mdx/macro';
import { htmlLang, i18n, type Language } from './i18n';
const blog = defineCollections({
type: 'doc',
@@ -11,6 +12,7 @@ const blog = defineCollections({
export const blogSource = loader({
baseUrl: '/blog',
source: blog.toFumadocsSource(),
i18n,
});
export function getBlogDate(path: string): Date {
@@ -19,8 +21,8 @@ export function getBlogDate(path: string): Date {
return match ? new Date(`${match[1]}T00:00:00Z`) : new Date(0);
}
export function formatBlogDate(path: string): string {
return new Intl.DateTimeFormat('zh-CN', {
export function formatBlogDate(path: string, lang: Language = i18n.defaultLanguage): string {
return new Intl.DateTimeFormat(htmlLang[lang], {
year: 'numeric',
month: 'long',
day: 'numeric',
+39
View File
@@ -0,0 +1,39 @@
import { gitConfig } from './shared';
export type RepoStats = {
stars: number | null;
version: string | null;
};
async function fetchJson(url: string): Promise<Record<string, unknown> | null> {
try {
const res = await fetch(url, {
headers: { Accept: 'application/vnd.github+json' },
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return (await res.json()) as Record<string, unknown>;
} catch {
return null;
}
}
export async function getRepoStats(): Promise<RepoStats> {
const base = `https://api.github.com/repos/${gitConfig.user}/${gitConfig.repo}`;
const [repo, release] = await Promise.all([
fetchJson(base),
fetchJson(`${base}/releases/latest`),
]);
return {
stars: typeof repo?.stargazers_count === 'number' ? repo.stargazers_count : null,
version: typeof release?.tag_name === 'string' ? release.tag_name : null,
};
}
export function formatStars(stars: number): string {
return new Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits: 1,
}).format(stars);
}
+25
View File
@@ -0,0 +1,25 @@
import { defineI18n } from 'fumadocs-core/i18n';
export const i18n = defineI18n({
defaultLanguage: 'zh',
languages: ['zh', 'en'],
hideLocale: 'default-locale',
});
export type Language = (typeof i18n)['languages'][number];
export function isLanguage(value: string): value is Language {
return i18n.languages.some((lang) => lang === value);
}
/** HTML `lang` attribute values. */
export const htmlLang: Record<Language, string> = {
zh: 'zh-CN',
en: 'en',
};
/** Locale ids understood by the desktop app / demo. */
export const demoLocale: Record<Language, string> = {
zh: 'zh-CN',
en: 'en-US',
};
+16 -8
View File
@@ -1,27 +1,35 @@
import { zhCN } from '@fumadocs/language/zh-cn';
import { defineTranslations } from 'fumadocs-core/i18n';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { i18n, type Language } from './i18n';
import { appName, gitConfig, releaseUrl } from './shared';
export const translations = defineTranslations().extend(uiTranslations()).preset(zhCN());
export const translations = i18n.translations().extend(uiTranslations()).preset('zh', zhCN());
const navLabels: Record<Language, { docs: string; blog: string; download: string }> = {
zh: { docs: '文档', blog: '开发者博客', download: '下载' },
en: { docs: 'Docs', blog: 'Blog', download: 'Download' },
};
export function baseOptions(lang: Language): BaseLayoutProps {
const labels = navLabels[lang];
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: appName,
},
links: [
{
text: '文档',
url: '/docs',
text: labels.docs,
url: `${prefix}/docs`,
},
{
text: '开发者博客',
url: '/blog',
text: labels.blog,
url: `${prefix}/blog`,
},
{
text: '下载',
text: labels.download,
url: releaseUrl,
external: true,
},
+13 -2
View File
@@ -3,6 +3,7 @@ import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
import { defineDocs } from 'fumadocs-mdx/macro';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
import { i18n } from './i18n';
const docs = defineDocs({
dir: 'content/docs',
@@ -22,14 +23,22 @@ export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
i18n,
});
/** Locale URL prefix segment; empty for the default (hidden) locale. */
function localeSegment(page: (typeof source)['$inferPage']): string | undefined {
return page.locale === i18n.defaultLanguage ? undefined : page.locale;
}
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
url:
'/' +
[localeSegment(page), ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
@@ -38,7 +47,9 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
return {
segments,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
url:
'/' +
[localeSegment(page), ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}