|
| 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 | +``` |
0 commit comments