Relevant source files
The following files were used as context for generating this wiki page:
- packages/base-ui/src/contexts/i18n.tsx
- packages/language/src/zh-tw.ts
- packages/radix-ui/src/contexts/i18n.tsx
- packages/base-ui/src/i18n.tsx
- packages/language/src/zh-cn.ts
- packages/radix-ui/src/i18n.tsx
- packages/preview/src/pages/...slugs.tsx
- packages/base-ui/src/contexts/tree.tsx
- packages/core/src/source/page-tree/builder.ts
- packages/base-ui/src/layouts/flux/index.tsx
- packages/core/src/source/llms.ts
- packages/radix-ui/src/layouts/flux/index.tsx
- packages/core/src/source/client/index.tsx
- packages/core/src/i18n/middleware.ts
- packages/base-ui/src/layouts/shared/slots/language-select.tsx
- packages/base-ui/src/layouts/flux/page/slots/footer.tsx
- packages/radix-ui/src/contexts/tree.tsx
- packages/core/src/source/loader.ts
- packages/base-ui/src/utils/use-footer-items.ts
- packages/radix-ui/src/utils/use-footer-items.ts
- packages/api-docs/src/i18n.ts
- packages/radix-ui/src/layouts/shared/slots/language-select.tsx
- packages/core/src/source/page-tree/transformer-fallback.ts
- packages/openapi/src/i18n.ts
- packages/preview/src/components/provider.tsx
- packages/epub/src/toc-builder.ts
- packages/preview/src/layouts/config.tsx
- packages/asyncapi/src/i18n.ts
- packages/base-ui/src/layouts/home/not-found.tsx
- packages/radix-ui/src/layouts/home/not-found.tsx
Overview
Fumadocs provides comprehensive internationalization support for multi-language documentation projects, coordinating server-side locale negotiation, request proxy middleware, and client React context providers. It bridges server-rendered storage collections with reactive UI components, enabling seamless language switching, localized navigation trees, and extensible translation dictionaries across base and Radix UI layouts.
Sources: packages/core/src/i18n/middleware.ts:56-110, packages/base-ui/src/contexts/i18n.tsx:33-72, packages/radix-ui/src/contexts/i18n.tsx:49-89, packages/base-ui/src/i18n.tsx:52-71, packages/core/src/source/loader.ts:303-346
Routing Middleware and Negotiation
Middleware Options and URL Formatting
The createI18nMiddleware function generates a Next.js proxy middleware to handle internationalization routing, language detection, and header negotiation based on MiddlewareOptions and core I18nConfig.
interface MiddlewareOptions extends I18nConfig {
format?: URLFormatter | ((locale: string, pathname: string) => string);
cookieName?: string;
}
export interface URLFormatter {
get: (url: NextURL) => string | undefined;
add: (url: NextURL, locale: string) => URL;
remove: (url: NextURL) => URL;
}Sources: packages/core/src/i18n/middleware.ts:7-36
| Option / Method | Type | Default | Purpose |
|---|---|---|---|
languages | string[] | Required | Supported language codes for the project |
defaultLanguage | string | Required | Fallback language used when negotiation fails or matches defaults |
format | URLFormatter | Function | DefaultFormatter | Formatter object or custom function to add, remove, or retrieve locale prefixes |
cookieName | string | 'FD_LOCALE' | Cookie key storing the active locale when hideLocale is set to 'always' |
hideLocale | 'never' | 'always' | 'default-locale' | 'never' | Strategy for hiding locale segments in the request pathname |
URLFormatter.get | (url: NextURL) => string | undefined | DefaultFormatter | Extracts the locale segment from the request URL pathname |
URLFormatter.add | (url: NextURL, locale: string) => URL | DefaultFormatter | Inserts the locale prefix and project base path into the URL |
URLFormatter.remove | (url: NextURL) => URL | DefaultFormatter | Strips the explicit locale segment from the URL pathname |
Sources: packages/core/src/i18n/middleware.ts:38-75
Request Routing and Negotiation Flow
When a request enters the proxy middleware, the handler extracts the current path locale using formatter.get(url). If the extracted segment is not present in the configured languages array, it is treated as undefined.
export const DefaultFormatter: URLFormatter = {
get(url) {
const segs = url.pathname.split('/');
if (segs.length > 1 && segs[1]) return segs[1];
},
add(url, locale) {
const next = new URL(url);
next.pathname = `${url.basePath}/${locale}/${url.pathname}`.replaceAll(/\/+/g, '/');
return next;
},
remove(url) {
const next = new URL(url);
const pathname = url.pathname.split('/').slice(2).join('/');
next.pathname = `${url.basePath}/${pathname}`.replaceAll(/\/+/g, '/');
return next;
},
};Sources: packages/core/src/i18n/middleware.ts:38-54
Depending on the configured hideLocale strategy, requests without a valid path locale trigger different routing behaviors:
- If
hideLocaleis'default-locale', the middleware rewrites the request to the URL formatted withdefaultLanguage. - If
hideLocaleis'always', it retrieves the locale from the request cookie specified bycookieName, falling back to language negotiation via@formatjs/intl-localematcherandgetNegotiator, then rewrites to the formatted URL. - Otherwise, it issues an HTTP redirect to the preferred negotiated locale URL.
if (!pathLocale) {
if (hideLocale === 'default-locale') {
return NextResponse.rewrite(formatter.add(url, defaultLanguage));
}
const finalLanguages = getNegotiator(request).languages(languages);
const preferred = matchLocale(finalLanguages, languages, defaultLanguage);
if (hideLocale === 'always') {
const locale = request.cookies.get(cookieName)?.value ?? preferred;
return NextResponse.rewrite(formatter.add(url, locale));
}
return NextResponse.redirect(formatter.add(url, preferred));
}Sources: packages/core/src/i18n/middleware.ts:82-96
Warning
When hideLocale is set to 'always' or 'default-locale' (when matching the default language), explicit locale prefixes in the URL are stripped via formatter.remove(url) and a redirect response is returned while simultaneously persisting the active locale code into the cookieName cookie.
Sources: packages/core/src/i18n/middleware.ts:98-106
Client Context and Localization Hooks
Translation APIs and Configuration Helpers
Client-side internationalization relies on React context providers and navigation hooks implemented identically across both packages/base-ui and packages/radix-ui. The LocaleContext supplies active locale state, available language items, and an onChange updater function accessed through the useI18n hook.
interface LocaleItem {
name: string;
locale: string;
}
interface LocaleContextType {
locale?: string;
onChange?: (v: string) => void;
locales?: LocaleItem[];
}
const LocaleContext = createContext<LocaleContextType>({});
export function useI18n(): LocaleContextType {
return use(LocaleContext);
}Sources: packages/base-ui/src/contexts/i18n.tsx:6-21, packages/radix-ui/src/contexts/i18n.tsx:6-21
The I18nProvider component wraps application trees with both LocaleContext and @fuma-translate/react's TranslationProvider. When onLocaleChange is omitted, the provider's built-in locale switcher mutates the current pathname by replacing or prepending the target language segment before calling router.push.
export function I18nProvider({
locales = [],
locale,
onLocaleChange,
children,
translations = Empty,
}: I18nProviderProps) {
const router = useRouter();
const pathname = usePathname();
const onChange = (value: string) => {
if (onLocaleChange) {
return onLocaleChange(value);
}
const segments = pathname.split('/').filter((v) => v.length > 0);
if (segments.length === 0 || segments[0] !== locale) {
segments.unshift(value);
} else {
segments[0] = value;
}
router.push(`/${segments.join('/')}`);
};Sources: packages/base-ui/src/contexts/i18n.tsx:33-55, packages/radix-ui/src/contexts/i18n.tsx:49-72
Note
I18nProvider maintains an onChangeRef synchronized with the latest onChange callback on every render, ensuring stable memoized context references passed to descendant components.
Sources: packages/base-ui/src/contexts/i18n.tsx:56-69, packages/radix-ui/src/contexts/i18n.tsx:73-86
Helper functions in packages/base-ui/src/i18n.tsx and packages/radix-ui/src/i18n.tsx bridge core translation dictionaries with UI provider properties. The uiTranslations extension injects localized UI keys, while i18nProvider resolves language fallbacks and maps supported language codes to display names.
| Function / Helper | Signature | Purpose |
|---|---|---|
uiTranslations | () => TranslationExtension<keyof Translations> | Returns compiled UI translation keys from package assets |
i18nProvider | (translations, lang?) => I18nProviderProps | Generates provider properties including locale, translations, and mapped locales array |
defineI18nUI | (config, localeTranslations) => I18nUIConfig | Extends an I18nConfig object with a custom provider factory method |
Sources: packages/base-ui/src/i18n.tsx:12-46, packages/radix-ui/src/i18n.tsx:12-46
Warning
When resolving language display names in i18nProvider, the implementation defaults to 'English' if t.get(code).displayName is undefined.
Sources: packages/base-ui/src/i18n.tsx:35-39, packages/radix-ui/src/i18n.tsx:35-39
Language Selection Interface Components
Language Switcher and Popover Presentation
The LanguageSelect component provides an interactive dropdown interface using primitive popover components. It retrieves the active language context via useI18n() and throws an error if context.locales is uninitialized. The trigger element renders with configurable button variants and applies an accessibility label through @fuma-translate/react.
export function LanguageSelect({
className,
variant = 'ghost',
children,
...rest
}: LanguageSelectProps): React.ReactElement {
const context = useI18n();
const t = useTranslations({ note: 'language switcher' });
if (!context.locales) throw new Error('Missing `<I18nProvider />`');
const chooseLanguage = t('Choose a language');
return (
<Popover>
<PopoverTrigger
aria-label={t('Choose a language', { note: 'aria-label' })}
className={(s) =>
cn(buttonVariants({ variant }), 'gap-1.5 p-1.5', s.open && 'bg-fd-accent', className)
}
{...rest}
>
{children}
</PopoverTrigger>
<PopoverContent className="flex flex-col gap-0.5 p-1">
<p className="p-2 text-xs font-medium text-fd-muted-foreground">{chooseLanguage}</p>Sources: packages/base-ui/src/layouts/shared/slots/language-select.tsx:14-38
export function LanguageSelect({
className,
variant = 'ghost',
children,
...rest
}: LanguageSelectProps): React.ReactElement {
const context = useI18n();
const t = useTranslations({ note: 'language switcher' });
if (!context.locales) throw new Error('Missing `<I18nProvider />`');
const chooseLanguage = t('Choose a language');
return (
<Popover>
<PopoverTrigger
aria-label={t('Choose a language', { note: 'aria-label' })}
className={cn(
buttonVariants({ variant }),
'gap-1.5 p-1.5 data-[state=open]:bg-fd-accent',
className,
)}
{...rest}
>
{children}
</PopoverTrigger>
<PopoverContent className="flex flex-col gap-0.5 p-1">
<p className="p-2 text-xs font-medium text-fd-muted-foreground">{chooseLanguage}</p>Sources: packages/radix-ui/src/layouts/shared/slots/language-select.tsx:14-40
Warning
Both package implementations require a parent <I18nProvider />; omitting the provider causes LanguageSelect to throw an error immediately during render when context.locales is missing.
Sources: packages/base-ui/src/layouts/shared/slots/language-select.tsx:22-22, packages/radix-ui/src/layouts/shared/slots/language-select.tsx:22-22
Locale Selection and Text Display
Inside the popover content, available locales are mapped into interactive buttons. Clicking an item triggers context.onChange?.(item.locale) to update the active language. The currently active locale receives distinct styling (bg-fd-primary/10 text-fd-primary), while unselected options display muted foreground colors with hover states.
<PopoverContent className="flex flex-col gap-0.5 p-1">
<p className="p-2 text-xs font-medium text-fd-muted-foreground">{chooseLanguage}</p>
{context.locales.map((item) => (
<button
key={item.locale}
type="button"
className={cn(
'px-2 py-1.5 text-start text-sm rounded-lg transition-colors',
item.locale === context.locale
? 'bg-fd-primary/10 text-fd-primary'
: 'text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
)}
onClick={() => {
context.onChange?.(item.locale);
}}
>
{item.name}
</button>
))}
</PopoverContent>Sources: packages/base-ui/src/layouts/shared/slots/language-select.tsx:37-56
<PopoverContent className="flex flex-col gap-0.5 p-1">
<p className="p-2 text-xs font-medium text-fd-muted-foreground">{chooseLanguage}</p>
{context.locales.map((item) => (
<button
key={item.locale}
type="button"
className={cn(
'px-2 py-1.5 text-start text-sm rounded-lg transition-colors',
item.locale === context.locale
? 'bg-fd-primary/10 text-fd-primary'
: 'text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
)}
onClick={() => {
context.onChange?.(item.locale);
}}
>
{item.name}
</button>
))}
</PopoverContent>Sources: packages/radix-ui/src/layouts/shared/slots/language-select.tsx:39-58
The companion LanguageSelectText component extracts the display name of the currently active locale from the i18n context and renders it inside a span element.
export type LanguageSelectTextProps = ComponentProps<'span'>;
export function LanguageSelectText(props: LanguageSelectTextProps) {
const { locales, locale } = useI18n();
const text = locales?.find((item) => item.locale === locale)?.name;
return <span {...props}>{text}</span>;
}Sources: packages/base-ui/src/layouts/shared/slots/language-select.tsx:61-68
export type LanguageSelectTextProps = ComponentProps<'span'>;
export function LanguageSelectText(props: LanguageSelectTextProps) {
const { locales, locale } = useI18n();
const text = locales?.find((item) => item.locale === locale)?.name;
return <span {...props}>{text}</span>;
}Sources: packages/radix-ui/src/layouts/shared/slots/language-select.tsx:63-70
Translation Presets and Extension Schemas
Structured Dictionary Bundles and Extension Extensions
Fumadocs structures internationalization dictionaries by combining base UI translations with extension tokens from specialized domain packages. Packages like @fumadocs/api-docs, fumadocs-openapi, and @fumadocs/asyncapi export dedicated translation extension functions that load compiled JSON translation keys and return a TranslationExtension object matching keyof Translations. For instance, apiDocsTranslations() reads from @/.translations/keys.json to yield typed translation keys for schema UIs and playgrounds.
import translationKeys from '@/.translations/keys.json';
import type { TranslationExtension } from 'fumadocs-core/i18n';
import type { Translations } from '@/.translations';
export type { Translations };
export function apiDocsTranslations(): TranslationExtension<keyof Translations> {
return { keys: translationKeys as never };
}Sources: packages/api-docs/src/i18n.ts:1-8
Domain-specific packages such as openapiTranslations() and asyncapiTranslations() compose their own translation keys with shared schema UI keys by invoking apiDocsTranslations() internally and spreading both key arrays into a unified extension object.
import translationKeys from '@/.translations/keys.json';
import type { TranslationExtension } from 'fumadocs-core/i18n';
import type { Translations } from '@/.translations';
import {
type Translations as SharedTranslations,
apiDocsTranslations,
} from '@fumadocs/api-docs/i18n';
export type { Translations };
export function openapiTranslations(): TranslationExtension<
keyof Translations | keyof SharedTranslations
> {
const shared = apiDocsTranslations();
return { keys: [...shared.keys, ...translationKeys] as never };
}Sources: packages/openapi/src/i18n.ts:1-16, packages/asyncapi/src/i18n.ts:1-15
Note
The openapiTranslations() function merges shared schema translation keys from @fumadocs/api-docs with OpenAPI-specific keys, ensuring schema UI components and playground controls resolve their labels correctly in multilingual builds.
Sources: packages/openapi/src/i18n.ts:11-16
Multilingual Bundling and Legacy Configuration
Multilingual dictionary bundling is illustrated by comprehensive locale definitions such as Traditional Chinese (zh-tw.ts) and Simplified Chinese (zh-cn.ts). These language files aggregate type definitions across all dependent packages and supply localized strings for user interfaces, API playgrounds, OpenAPI operations, and AsyncAPI channels.
import type { TranslationPreset } from 'fumadocs-core/i18n';
import type { Translations as UITranslations } from 'fumadocs-ui/i18n';
import type { Translations as OpenAPITranslations } from 'fumadocs-openapi/i18n';
import type { Translations as AsyncAPITranslations } from '@fumadocs/asyncapi/i18n';
import type { Translations as StoryTranslations } from '@fumadocs/story/i18n';
import type { Translations as APIDocsTranslations } from '@fumadocs/api-docs/i18n';
type Translations = UITranslations &
OpenAPITranslations &
AsyncAPITranslations &
StoryTranslations &
APIDocsTranslations;Sources: packages/language/src/zh-tw.ts:1-12, packages/language/src/zh-cn.ts:1-12
For backward compatibility with legacy setups, defineI18nOpenAPI wraps an existing I18nUIConfig instance, overriding its provider method to inject partial translation dictionaries for specific locales dynamically during runtime localization lookup.
export function defineI18nOpenAPI<Languages extends string>(
config: I18nUIConfig<Languages>,
translations: Partial<Record<NoInfer<Languages>, Partial<Translations>>>,
): I18nUIConfig<Languages> {
return {
...config,
provider(locale = config.defaultLanguage) {
const out = config.provider(locale);
const data = translations[locale as Languages];
if (data) {
out.translations = { ...out.translations, ...data };
}
return out;
},
};
}Sources: packages/openapi/src/i18n.ts:19-33
Warning
The defineI18nOpenAPI helper is marked as deprecated; developers are advised to use i18n.translations() combined with openapiTranslations() instead of wrapping the i18n UI configuration object.
Sources: packages/openapi/src/i18n.ts:18-18
Localized Tree Navigation and Pagination
Tree Context Resolution and Active Page Traversal
The TreeContextProvider component initializes tree navigation by wrapping raw page trees and resolving active hierarchies. It tracks the current location via usePathname() and computes the active breadcrumb path by invoking searchPath(tree.children, pathname), falling back to tree.fallback?.children if necessary.
export function TreeContextProvider({
tree: rawTree,
children,
}: {
tree: PageTree.Root;
children: ReactNode;
}) {
const nextIdRef = useRef(0);
const pathname = usePathname();
const tree = useMemo(() => rawTree, [rawTree.$id]);
const path = useMemo(() => {
return (
searchPath(tree.children, pathname) ??
(tree.fallback ? searchPath(tree.fallback.children, pathname) : null) ??
[]
);
}, [tree, pathname]);
const root = path.findLast((item) => item.type === 'folder' && item.root) ?? tree;
root.$id ??= String(nextIdRef.current++);
return (
<TreeContext value={useMemo(() => ({ root, full: tree }) as TreeContextType, [root, tree])}>
<PathContext value={path}>{children}</PathContext>
</TreeContext>
);
}Sources: packages/base-ui/src/contexts/tree.tsx:17-47, packages/radix-ui/src/contexts/tree.tsx:17-47
Note
Object-typed tree props passed from Server Components can be re-constructed across renders, breaking hook dependencies. TreeContextProvider mitigates this by memoizing on rawTree.$id and assigning a fallback identifier using nextIdRef if $id is uninitialized.
Sources: packages/base-ui/src/contexts/tree.tsx:27-40, packages/radix-ui/src/contexts/tree.tsx:27-40
Footer Pagination Linking Across Page Trees
The useFooterItems() hook flattens the active tree root into a linear sequence of non-external page nodes, caching the result in a WeakMap indexed by PageTree.Root or PageTree.Folder. Folder index nodes are traversed first before processing child entries.
const footerCache = new WeakMap<PageTree.Root | PageTree.Folder, PageTree.Item[]>();
export function useFooterItems(): PageTree.Item[] {
const { root } = useTreeContext();
const cached = footerCache.get(root);
if (cached) return cached;
const list: PageTree.Item[] = [];
function onNode(node: PageTree.Node) {
if (node.type === 'folder') {
if (node.index) onNode(node.index);
for (const child of node.children) onNode(child);
} else if (node.type === 'page' && !node.external) {
list.push(node);
}
}
for (const child of root.children) onNode(child);
footerCache.set(root, list);
return list;
}Sources: packages/base-ui/src/utils/use-footer-items.ts:5-27, packages/radix-ui/src/utils/use-footer-items.ts:5-27
The Footer component consumes this flattened array, locating the active index via isActive(item.url, pathname) to supply previous and next page references for pagination links.
export function Footer({ items, children, className, ...props }: FooterProps) {
const footerList = useFooterItems();
const pathname = usePathname();
const { previous, next } = useMemo(() => {
if (items) return items;
const idx = footerList.findIndex((item) => isActive(item.url, pathname));
if (idx === -1) return {};
return {
previous: footerList[idx - 1],
next: footerList[idx + 1],
};
}, [footerList, items, pathname]);
return (
<>
<div
className={cn(
'@container grid gap-4',
previous && next ? 'grid-cols-2' : 'grid-cols-1',
className,
)}
{...props}
>
{previous && <FooterItem item={previous} index={0} />}
{next && <FooterItem item={next} index={1} />}
</div>
{children}
</>
);
}Sources: packages/base-ui/src/layouts/flux/page/slots/footer.tsx:25-56
Related
On This Page
OverviewRouting Middleware and NegotiationMiddleware Options and URL FormattingRequest Routing and Negotiation FlowClient Context and Localization HooksTranslation APIs and Configuration HelpersLanguage Selection Interface ComponentsLanguage Switcher and Popover PresentationLocale Selection and Text DisplayTranslation Presets and Extension SchemasStructured Dictionary Bundles and Extension ExtensionsMultilingual Bundling and Legacy ConfigurationLocalized Tree Navigation and PaginationTree Context Resolution and Active Page TraversalFooter Pagination Linking Across Page TreesRelated