Skip to content

Commit 70f75cd

Browse files
authored
feat: add i18n support for 8 languages (#100)
Add internationalization infrastructure using Fumadocs i18n with support for English, French, German, Spanish, Japanese, Russian, Simplified Chinese, and Traditional Chinese. - Create i18n config with defineI18n (en, fr, de, es, ja, ru, cn, tw) - Restructure app routes under [lang] dynamic segment - Pass i18n config to fumadocs-core source loader - Update layouts to be locale-aware (RootProvider, DocsLayout, shared) - Add root redirect from / to /en - Update LLM and OG routes for locale support - Add cyrillic font subset for Russian support
1 parent 6835c9a commit 70f75cd

14 files changed

Lines changed: 166 additions & 84 deletions

File tree

src/app/(home)/layout.tsx

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/app/(home)/page.tsx

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/app/[lang]/(home)/layout.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { HomeLayout } from 'fumadocs-ui/layouts/home';
2+
import { baseOptions } from '@/lib/layout.shared';
3+
4+
export default async function Layout({
5+
children,
6+
params,
7+
}: {
8+
children: React.ReactNode;
9+
params: Promise<{ lang: string }>;
10+
}) {
11+
const { lang } = await params;
12+
13+
return <HomeLayout {...baseOptions(lang)}>{children}</HomeLayout>;
14+
}

src/app/[lang]/(home)/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { redirect } from 'next/navigation';
2+
3+
export default async function HomePage({
4+
params,
5+
}: {
6+
params: Promise<{ lang: string }>;
7+
}) {
8+
const { lang } = await params;
9+
redirect(`/${lang}/docs`);
10+
}
Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@ import { notFound } from 'next/navigation';
99
import { getMDXComponents } from '@/mdx-components';
1010
import type { Metadata } from 'next';
1111
import { createRelativeLink } from 'fumadocs-ui/mdx';
12+
import { i18n } from '@/lib/i18n';
1213

13-
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
14+
export default async function Page(props: {
15+
params: Promise<{ lang: string; slug?: string[] }>;
16+
}) {
1417
const params = await props.params;
15-
const page = source.getPage(params.slug);
18+
const page = source.getPage(params.slug, params.lang);
1619
if (!page) notFound();
1720

1821
const MDX = page.data.body;
1922
const path = `content/docs/${params.slug?.join('/') || 'index'}.mdx`;
2023

2124
return (
22-
<DocsPage
23-
toc={page.data.toc}
25+
<DocsPage
26+
toc={page.data.toc}
2427
full={page.data.full}
2528
editOnGithub={{
2629
owner: 'BasisVR',
@@ -34,7 +37,6 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
3437
<DocsBody>
3538
<MDX
3639
components={getMDXComponents({
37-
// this allows you to link to other pages with relative file paths
3840
a: createRelativeLink(source, page),
3941
})}
4042
/>
@@ -43,15 +45,20 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
4345
);
4446
}
4547

46-
export async function generateStaticParams() {
47-
return source.generateParams();
48+
export function generateStaticParams() {
49+
return i18n.languages.flatMap((lang) =>
50+
source.getPages(lang).map((page) => ({
51+
lang,
52+
slug: page.slugs,
53+
})),
54+
);
4855
}
4956

50-
export async function generateMetadata(
51-
props: PageProps<'/docs/[[...slug]]'>,
52-
): Promise<Metadata> {
57+
export async function generateMetadata(props: {
58+
params: Promise<{ lang: string; slug?: string[] }>;
59+
}): Promise<Metadata> {
5360
const params = await props.params;
54-
const page = source.getPage(params.slug);
61+
const page = source.getPage(params.slug, params.lang);
5562
if (!page) notFound();
5663

5764
return {
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
33
import { baseOptions } from '@/lib/layout.shared';
44
import type { ReactNode } from 'react';
55

6-
export default function Layout({ children }: { children: ReactNode }) {
6+
export default async function Layout({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: Promise<{ lang: string }>;
12+
}) {
13+
const { lang } = await params;
14+
715
return (
8-
<DocsLayout
9-
tree={source.pageTree}
10-
{...baseOptions()}
16+
<DocsLayout
17+
tree={source.pageTree[lang]}
18+
{...baseOptions(lang)}
1119
sidebar={{
1220
defaultOpenLevel: 0,
1321
footer: (

src/app/[lang]/layout.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { RootProvider } from 'fumadocs-ui/provider/next';
2+
import type { Metadata } from 'next';
3+
import { i18n, localeDisplayNames } from '@/lib/i18n';
4+
5+
export const metadata: Metadata = {
6+
title: {
7+
default: 'BasisVR Docs',
8+
template: '%s | BasisVR Docs',
9+
},
10+
description: 'Documentation for BasisVR - Virtual Reality Framework',
11+
metadataBase: new URL('https://docs.basisvr.org'),
12+
openGraph: {
13+
title: 'BasisVR Docs',
14+
description: 'Documentation for BasisVR - Virtual Reality Framework',
15+
url: 'https://docs.basisvr.org',
16+
siteName: 'BasisVR Docs',
17+
images: [
18+
{
19+
url: '/img/basisvr-social-card.jpg',
20+
width: 1200,
21+
height: 630,
22+
alt: 'BasisVR',
23+
},
24+
],
25+
type: 'website',
26+
},
27+
twitter: {
28+
card: 'summary_large_image',
29+
title: 'BasisVR Docs',
30+
description: 'Documentation for BasisVR - Virtual Reality Framework',
31+
images: ['/img/basisvr-social-card.jpg'],
32+
},
33+
icons: {
34+
icon: '/img/BasisLogoSmall.png',
35+
},
36+
};
37+
38+
export default async function LangLayout({
39+
children,
40+
params,
41+
}: {
42+
children: React.ReactNode;
43+
params: Promise<{ lang: string }>;
44+
}) {
45+
const { lang } = await params;
46+
47+
return (
48+
<RootProvider
49+
locale={lang}
50+
i18n={{
51+
locale: lang,
52+
locales: i18n.languages.map((l) => ({
53+
locale: l,
54+
name: localeDisplayNames[l] ?? l,
55+
})),
56+
}}
57+
search={{
58+
enabled: false,
59+
}}
60+
theme={{
61+
defaultTheme: 'dark',
62+
}}
63+
>
64+
{children}
65+
</RootProvider>
66+
);
67+
}
68+
69+
export function generateStaticParams() {
70+
return i18n.languages.map((lang) => ({ lang }));
71+
}

src/app/layout.tsx

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,18 @@
1-
import { RootProvider } from 'fumadocs-ui/provider/next';
2-
import type { Metadata } from 'next';
31
import './global.css';
42
import { Inter } from 'next/font/google';
53

64
const inter = Inter({
7-
subsets: ['latin'],
5+
subsets: ['latin', 'latin-ext', 'cyrillic'],
86
});
97

10-
export const metadata: Metadata = {
11-
title: {
12-
default: 'BasisVR Docs',
13-
template: '%s | BasisVR Docs',
14-
},
15-
description: 'Documentation for BasisVR - Virtual Reality Framework',
16-
metadataBase: new URL('https://docs.basisvr.org'),
17-
openGraph: {
18-
title: 'BasisVR Docs',
19-
description: 'Documentation for BasisVR - Virtual Reality Framework',
20-
url: 'https://docs.basisvr.org',
21-
siteName: 'BasisVR Docs',
22-
images: [
23-
{
24-
url: '/img/basisvr-social-card.jpg',
25-
width: 1200,
26-
height: 630,
27-
alt: 'BasisVR',
28-
},
29-
],
30-
locale: 'en',
31-
type: 'website',
32-
},
33-
twitter: {
34-
card: 'summary_large_image',
35-
title: 'BasisVR Docs',
36-
description: 'Documentation for BasisVR - Virtual Reality Framework',
37-
images: ['/img/basisvr-social-card.jpg'],
38-
},
39-
icons: {
40-
icon: '/img/BasisLogoSmall.png',
41-
},
42-
};
43-
44-
export default function Layout({ children }: LayoutProps<'/'>) {
8+
export default function RootLayout({
9+
children,
10+
}: {
11+
children: React.ReactNode;
12+
}) {
4513
return (
46-
<html lang="en" className={inter.className} suppressHydrationWarning>
47-
<body className="flex flex-col min-h-screen">
48-
<RootProvider
49-
search={{
50-
enabled: false,
51-
}}
52-
theme={{
53-
defaultTheme: 'dark',
54-
}}
55-
>
56-
{children}
57-
</RootProvider>
58-
</body>
14+
<html suppressHydrationWarning className={inter.className}>
15+
<body className="flex flex-col min-h-screen">{children}</body>
5916
</html>
6017
);
6118
}

src/app/llms-full.txt/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getLLMText, source } from '@/lib/source';
33
export const revalidate = false;
44

55
export async function GET() {
6-
const scan = source.getPages().map(getLLMText);
6+
const scan = source.getPages('en').map(getLLMText);
77
const scanned = await Promise.all(scan);
88

99
return new Response(scanned.join('\n\n'));

src/app/og/docs/[...slug]/route.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getPageImage, source } from '@/lib/source';
22
import { notFound } from 'next/navigation';
33
import { ImageResponse } from 'next/og';
44
import { generate as DefaultImage } from 'fumadocs-ui/og';
5+
import { i18n } from '@/lib/i18n';
56

67
export const revalidate = false;
78

@@ -28,8 +29,9 @@ export async function GET(
2829
}
2930

3031
export function generateStaticParams() {
31-
return source.getPages().map((page) => ({
32-
lang: page.locale,
33-
slug: getPageImage(page).segments,
34-
}));
32+
return i18n.languages.flatMap((lang) =>
33+
source.getPages(lang).map((page) => ({
34+
slug: getPageImage(page).segments,
35+
})),
36+
);
3537
}

0 commit comments

Comments
 (0)