---
title: "Internationalization Support"
description: "Fumadocs provides comprehensive internationalization support for multi-language documentation projects, coordinating server-side locale negotiation, request proxy middleware, and client React conte..."
last_updated: "2026-10-12T07:53:59.827853+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/internationalization-support"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/base-ui/src/contexts/i18n.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/i18n.tsx)
- [packages/language/src/zh-tw.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/language/src/zh-tw.ts)
- [packages/radix-ui/src/contexts/i18n.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/i18n.tsx)
- [packages/base-ui/src/i18n.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/i18n.tsx)
- [packages/language/src/zh-cn.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/language/src/zh-cn.ts)
- [packages/radix-ui/src/i18n.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/i18n.tsx)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/base-ui/src/contexts/tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/tree.tsx)
- [packages/core/src/source/page-tree/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts)
- [packages/base-ui/src/layouts/flux/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/index.tsx)
- [packages/core/src/source/llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/llms.ts)
- [packages/radix-ui/src/layouts/flux/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/index.tsx)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/core/src/i18n/middleware.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts)
- [packages/base-ui/src/layouts/shared/slots/language-select.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/slots/language-select.tsx)
- [packages/base-ui/src/layouts/flux/page/slots/footer.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/footer.tsx)
- [packages/radix-ui/src/contexts/tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx)
- [packages/core/src/source/loader.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts)
- [packages/base-ui/src/utils/use-footer-items.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/utils/use-footer-items.ts)
- [packages/radix-ui/src/utils/use-footer-items.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts)
- [packages/api-docs/src/i18n.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/i18n.ts)
- [packages/radix-ui/src/layouts/shared/slots/language-select.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/slots/language-select.tsx)
- [packages/core/src/source/page-tree/transformer-fallback.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts)
- [packages/openapi/src/i18n.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/i18n.ts)
- [packages/preview/src/components/provider.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/provider.tsx)
- [packages/epub/src/toc-builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/epub/src/toc-builder.ts)
- [packages/preview/src/layouts/config.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/layouts/config.tsx)
- [packages/asyncapi/src/i18n.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/i18n.ts)
- [packages/base-ui/src/layouts/home/not-found.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/home/not-found.tsx)
- [packages/radix-ui/src/layouts/home/not-found.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/home/not-found.tsx)
</details>

## 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L56-L110), [packages/base-ui/src/contexts/i18n.tsx:33-72](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/i18n.tsx#L33-L72), [packages/radix-ui/src/contexts/i18n.tsx:49-89](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/i18n.tsx#L49-L89), [packages/base-ui/src/i18n.tsx:52-71](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/i18n.tsx#L52-L71), [packages/core/src/source/loader.ts:303-346](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L303-L346)

## 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`.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L7-L36)

| 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L38-L75)

### 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L38-L54)

Depending on the configured `hideLocale` strategy, requests without a valid path locale trigger different routing behaviors:
- If `hideLocale` is `'default-locale'`, the middleware rewrites the request to the URL formatted with `defaultLanguage`.
- If `hideLocale` is `'always'`, it retrieves the locale from the request cookie specified by `cookieName`, falling back to language negotiation via `@formatjs/intl-localematcher` and `getNegotiator`, then rewrites to the formatted URL.
- Otherwise, it issues an HTTP redirect to the preferred negotiated locale URL.

```typescript
    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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L82-L96)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/i18n/middleware.ts#L98-L106)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/i18n.tsx#L6-L21), [packages/radix-ui/src/contexts/i18n.tsx:6-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/i18n.tsx#L6-L21)

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`.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/i18n.tsx#L33-L55), [packages/radix-ui/src/contexts/i18n.tsx:49-72](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/i18n.tsx#L49-L72)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/i18n.tsx#L56-L69), [packages/radix-ui/src/contexts/i18n.tsx:73-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/i18n.tsx#L73-L86)

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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/i18n.tsx#L12-L46), [packages/radix-ui/src/i18n.tsx:12-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/i18n.tsx#L12-L46)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/i18n.tsx#L35-L39), [packages/radix-ui/src/i18n.tsx:35-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/i18n.tsx#L35-L39)

## 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`.

```tsx
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/slots/language-select.tsx#L14-L38)

```tsx
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/slots/language-select.tsx#L14-L40)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/slots/language-select.tsx#L22-L22), [packages/radix-ui/src/layouts/shared/slots/language-select.tsx:22-22](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/slots/language-select.tsx#L22-L22)

### 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.

```tsx
<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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/slots/language-select.tsx#L37-L56)

```tsx
<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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/slots/language-select.tsx#L39-L58)

The companion `LanguageSelectText` component extracts the display name of the currently active locale from the i18n context and renders it inside a span element.

```tsx
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/slots/language-select.tsx#L61-L68)

```tsx
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/slots/language-select.tsx#L63-L70)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/i18n.ts#L1-L8)

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.

```typescript
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](https://github.com/blade47/fumadox/blob/HEAD/packages/openapi/src/i18n.ts#L1-L16), [packages/asyncapi/src/i18n.ts:1-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/i18n.ts#L1-L15)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/i18n.ts#L11-L16)

### 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/language/src/zh-tw.ts#L1-L12), [packages/language/src/zh-cn.ts:1-12](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/language/src/zh-cn.ts#L1-L12)

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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/i18n.ts#L19-L33)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/i18n.ts#L18-L18)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/tree.tsx#L17-L47), [packages/radix-ui/src/contexts/tree.tsx:17-47](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L17-L47)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/tree.tsx#L27-L40), [packages/radix-ui/src/contexts/tree.tsx:27-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L27-L40)

### 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/utils/use-footer-items.ts#L5-L27), [packages/radix-ui/src/utils/use-footer-items.ts:5-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L5-L27)

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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/footer.tsx#L25-L56)

## Related

- [Page Tree Generation](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/page-tree-generation)
- [Shared Layout Architecture](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/shared-layout-architecture)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
