Skip to content

Commit c0ad948

Browse files
committed
feat(i18n): add new language support and enhance documentation
- Introduced new documentation files for MultiPost in Spanish, French, Indonesian, Japanese, Korean, and Russian. - Updated existing translations and added new language support in the i18n configuration. - Enhanced layout components to accommodate the new multilingual features.
1 parent bdc9466 commit c0ad948

12 files changed

+1214
-33
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
# Fumadocs Framework: Internationalization
7+
URL: /docs/ui/internationalization
8+
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/internationalization.mdx
9+
10+
Support multiple languages in your documentation
11+
12+
## Overview
13+
14+
For Next.js apps, you'll have to configure i18n routing on both Next.js and Fumadocs.
15+
16+
Fumadocs is not a full-powered i18n library, it's up to you when implementing i18n for Next.js part.
17+
You can also use other libraries with Fumadocs like [next-intl](mdc:https:/github.com/amannn/next-intl).
18+
19+
[Learn more about i18n in Next.js](mdc:https:/nextjs.org/docs/app/building-your-application/routing/internationalization).
20+
21+
## Setup
22+
23+
Define the i18n configurations in a file, we will import it with `@/ilb/i18n` in this guide.
24+
25+
```ts title="lib/i18n.ts"
26+
import type { I18nConfig } from 'fumadocs-core/i18n';
27+
28+
export const i18n: I18nConfig = {
29+
defaultLanguage: 'en',
30+
languages: ['en', 'cn'],
31+
};
32+
33+
```
34+
35+
> See [customisable options](mdc:docs/headless/internationalization).
36+
37+
Pass it to the source loader.
38+
39+
```ts title="lib/source.ts"
40+
import { i18n } from '@/lib/i18n';
41+
import { loader } from 'fumadocs-core/source';
42+
43+
export const source = loader({
44+
i18n, // [!code ++]
45+
// other options
46+
});
47+
```
48+
49+
### Middleware
50+
51+
Create a middleware that redirects users to appropriate locale.
52+
53+
```ts title="middleware.ts"
54+
import { createI18nMiddleware } from 'fumadocs-core/i18n';
55+
import { i18n } from '@/lib/i18n';
56+
57+
export default createI18nMiddleware(i18n);
58+
59+
export const config = {
60+
// Matcher ignoring `/_next/` and `/api/`
61+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
62+
};
63+
64+
```
65+
66+
<Callout title="Custom Middleware">
67+
The default middleware is optional, you can instead use your own middleware or the one provided by i18n libraries.
68+
69+
When using custom middleware, make sure the locale is correctly passed to Fumadocs.
70+
You may also want to [customise page URLs](mdc:docs/headless/source-api#url) from `loader()`.
71+
</Callout>
72+
73+
### Routing
74+
75+
Create a `/app/[lang]` folder, and move all files (e.g. `page.tsx`, `layout.tsx`) from `/app` to the folder.
76+
77+
Provide UI translations and other config to `<RootProvider />`.
78+
Note that only English translations are provided by default.
79+
80+
```tsx title="app/[lang]/layout.tsx"
81+
import { RootProvider } from 'fumadocs-ui/provider';
82+
import type { Translations } from 'fumadocs-ui/i18n';
83+
84+
// translations
85+
const cn: Partial<Translations> = {
86+
search: 'Translated Content',
87+
};
88+
89+
// available languages that will be displayed on UI
90+
// make sure `locale` is consistent with your i18n config
91+
const locales = [
92+
{
93+
name: 'English',
94+
locale: 'en',
95+
},
96+
{
97+
name: 'Chinese',
98+
locale: 'cn',
99+
},
100+
];
101+
102+
export default async function RootLayout({
103+
params,
104+
children,
105+
}: {
106+
params: Promise<{ lang: string }>;
107+
children: React.ReactNode;
108+
}) {
109+
const lang = (await params).lang;
110+
111+
return (
112+
<html lang={lang}>
113+
<body>
114+
<RootProvider
115+
i18n={{
116+
locale: lang, // [!code ++]
117+
locales, // [!code ++]
118+
translations: { cn }[lang], // [!code ++]
119+
}}
120+
>
121+
{children}
122+
</RootProvider>
123+
</body>
124+
</html>
125+
);
126+
}
127+
```
128+
129+
### Pass Locale
130+
131+
Pass the locale to Fumadocs in your pages and layouts.
132+
133+
```tsx title="app/layout.config.tsx" tab="Shared Options"
134+
import { i18n } from '@/lib/i18n';
135+
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
136+
137+
// Make `baseOptions` a function: [!code highlight]
138+
export function baseOptions(locale: string): BaseLayoutProps {
139+
return {
140+
i18n, // [!code ++]
141+
// different props based on `locale`
142+
};
143+
}
144+
```
145+
146+
```tsx title="/app/[lang]/(home)/layout.tsx" tab="Home Layout"
147+
import type { ReactNode } from 'react';
148+
import { HomeLayout } from 'fumadocs-ui/layouts/home';
149+
import { baseOptions } from '@/app/layout.config';
150+
151+
export default async function Layout({
152+
params,
153+
children,
154+
}: {
155+
params: Promise<{ lang: string }>;
156+
children: ReactNode;
157+
}) {
158+
const { lang } = await params;
159+
160+
return <HomeLayout {...baseOptions(lang)}>{children}</HomeLayout>; // [!code highlight]
161+
}
162+
```
163+
164+
```tsx title="/app/[lang]/docs/layout.tsx" tab="Docs Layout"
165+
import type { ReactNode } from 'react';
166+
import { source } from '@/lib/source';
167+
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
168+
import { baseOptions } from '@/app/layout.config';
169+
170+
export default async function Layout({
171+
params,
172+
children,
173+
}: {
174+
params: Promise<{ lang: string }>;
175+
children: ReactNode;
176+
}) {
177+
const { lang } = await params;
178+
179+
return (
180+
// [!code highlight]
181+
<DocsLayout {...baseOptions(lang)} tree={source.pageTree[lang]}>
182+
{children}
183+
</DocsLayout>
184+
);
185+
}
186+
```
187+
188+
```ts title="page.tsx" tab="Docs Page"
189+
import { source } from '@/lib/source';
190+
191+
export default async function Page({
192+
params,
193+
}: {
194+
params: Promise<{ lang: string; slug?: string[] }>;
195+
}) {
196+
const { slug, lang } = await params;
197+
// get page
198+
source.getPage(slug); // [!code --]
199+
source.getPage(slug, lang); // [!code ++]
200+
201+
// get pages
202+
source.getPages(); // [!code --]
203+
source.getPages(lang); // [!code ++]
204+
}
205+
```
206+
207+
<Callout title={<>Using another name for <code>lang</code> dynamic segment?</>}>
208+
If you're using another name like `app/[locale]`, you also need to update `generateStaticParams()` in docs page:
209+
210+
```tsx
211+
export function generateStaticParams() {
212+
return source.generateParams(); // [!code --]
213+
return source.generateParams('slug', 'locale'); // [!code ++] new param name
214+
}
215+
```
216+
</Callout>
217+
218+
### Search
219+
220+
Configure i18n on your search solution.
221+
222+
* **Built-in Search (Orama):**
223+
For [Supported Languages](mdc:https:/docs.orama.com/open-source/supported-languages#officially-supported-languages), no further changes are needed.
224+
225+
Otherwise, additional config is required (e.g. Chinese & Japanese). See [Special Languages](mdc:docs/headless/search/orama#special-languages).
226+
227+
* **Cloud Solutions (e.g. Algolia):**
228+
They usually have official support for multilingual.
229+
230+
## Writing Documents
231+
232+
You can add Markdown/meta files for different languages by attending `.{locale}` to your file name, like `page.cn.md` and `meta.cn.json`.
233+
234+
Make sure to create a file for the default locale first, the locale code is optional (e.g. both `get-started.mdx` and `get-started.en.mdx` are accepted).
235+
236+
<Files>
237+
<Folder name="content/docs" defaultOpen>
238+
<File name="meta.json" />
239+
240+
<File name="meta.cn.json" />
241+
242+
<File name="get-started.mdx" />
243+
244+
<File name="get-started.cn.mdx" />
245+
</Folder>
246+
</Files>
247+
248+
## Navigation
249+
250+
Fumadocs only handles navigation for its own layouts (e.g. sidebar).
251+
For other places, you can use the `useParams` hook to get the locale from url, and attend it to `href`.
252+
253+
```tsx
254+
import Link from 'next/link';
255+
import { useParams } from 'next/navigation';
256+
257+
const { lang } = useParams();
258+
259+
return <Link href={`/${lang}/another-page`}>This is a link</Link>;
260+
```
261+
262+
In addition, the [`fumadocs-core/dynamic-link`](mdc:docs/headless/components/link#dynamic-hrefs) component supports dynamic hrefs, you can use it to attend the locale prefix.
263+
It is useful for Markdown/MDX content.
264+
265+
```mdx title="content.mdx"
266+
import { DynamicLink } from 'fumadocs-core/dynamic-link';
267+
268+
<DynamicLink href="/[lang]/another-page">This is a link</DynamicLink>
269+
```

.cursor/rules/metadata.mdc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
# Fumadocs Framework: Metadata
7+
URL: /docs/ui/open-graph
8+
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/(integrations)/open-graph.mdx
9+
10+
Usage with Next.js Metadata API
11+
12+
## Introduction
13+
14+
Next.js provides an useful set of utilities, allowing a flexible experience with Fumadocs.
15+
Fumadocs uses the Next.js Metadata API for SEO.
16+
17+
Make sure to read their [Metadata section](https://nextjs.org/docs/app/building-your-application/optimizing/metadata) for the fundamentals of Metadata API.
18+
19+
## Open Graph Image
20+
21+
For docs pages, Fumadocs has a built-in metadata image generator.
22+
23+
You will need a route handler to get started.
24+
25+
```tsx title="app/docs-og/[...slug]/route.tsx"
26+
import { generateOGImage } from 'fumadocs-ui/og';
27+
import { source } from '@/lib/source';
28+
import { notFound } from 'next/navigation';
29+
30+
export async function GET(
31+
_req: Request,
32+
{ params }: { params: Promise<{ slug: string[] }> },
33+
) {
34+
const { slug } = await params;
35+
const page = source.getPage(slug.slice(0, -1));
36+
if (!page) notFound();
37+
38+
return generateOGImage({
39+
title: page.data.title,
40+
description: page.data.description,
41+
site: 'My App',
42+
});
43+
}
44+
45+
export function generateStaticParams() {
46+
return source.generateParams().map((page) => ({
47+
...page,
48+
slug: [...page.slug, 'image.png'],
49+
}));
50+
}
51+
52+
```
53+
54+
> We need to append `image.png` to the end of slugs so that we can access it via `/docs-og/my-page/image.png`.
55+
56+
In your docs page, add the image to metadata.
57+
58+
```tsx title="app/docs/[[...slug]]/page.tsx"
59+
import { notFound } from 'next/navigation';
60+
import { source } from '@/lib/source';
61+
62+
export async function generateMetadata({
63+
params,
64+
}: {
65+
params: Promise<{ slug?: string[] }>;
66+
}) {
67+
const { slug = [] } = await params;
68+
const page = source.getPage(slug);
69+
if (!page) notFound();
70+
71+
const image = ['/docs-og', ...slug, 'image.png'].join('/');
72+
return {
73+
title: page.data.title,
74+
description: page.data.description,
75+
openGraph: {
76+
images: image,
77+
},
78+
twitter: {
79+
card: 'summary_large_image',
80+
images: image,
81+
},
82+
};
83+
}
84+
```
85+
86+
### Font
87+
88+
You can also customise the font, options for Satori are also available on the built-in generator.
89+
90+
```ts
91+
import { generateOGImage } from 'fumadocs-ui/og';
92+
93+
generateOGImage({
94+
fonts: [
95+
{
96+
name: 'Roboto',
97+
// Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.
98+
data: robotoArrayBuffer,
99+
weight: 400,
100+
style: 'normal',
101+
},
102+
],
103+
});
104+
```

0 commit comments

Comments
 (0)