# Multilingual Sites

With `i18n` set in your Astro config, minidocs organizes content by locale and generates pages for every locale. This page covers what differs from a single-language site.

## Configuration

```js
// astro.config.mjs
export default defineConfig({
  i18n: { locales: ['zh', 'en'], defaultLocale: 'zh' },
  integrations: [mdx(), minidocs()],
});
```

URLs of the default locale have no locale segment, such as `/docs/`; other locales do, such as `/en/docs/`. To give the default locale one too, set Astro’s `routing: { prefixDefaultLocale: true }`.

`locales` must be plain strings, and `routing: 'manual'` is not supported yet. Declare how untranslated pages show with minidocs’ `fallback` below, not Astro’s `i18n.fallback`.

## Content

The first level of directories in a collection is the locale:

```
src/content/docs/
├── zh/
│   ├── index.mdx
│   └── guides/sidebar.mdx
└── en/
    ├── index.mdx
    └── guides/sidebar.mdx
```

Page ids leave the locale out, so `guides/sidebar.mdx` in both locales is two versions of the same page, `guides/sidebar`.

## Routes and data

Compared with a single-language site, three things differ:

- Route files gain a `[...locale]` level, such as `src/pages/[...locale]/docs/[...slug].astro`, and `page.params` gains `locale`
- `defineTree` returns one tree per locale, and you render `tree[locale]`
- `toHref` takes the locale: `toHref('docs', 'guides/sidebar', locale)`

Every page from `listPages` carries its `locale`, so the layout uses it directly:

```astro
---
const { id, locale } = Astro.props;
const nav = navState(tree[locale], id);
---

<html lang={locale}>
```

## Untranslated pages

Until a page is translated, it can show the content of another locale:

```js
minidocs({ fallback: { en: 'zh' } })
```

Pages missing in English then show the Chinese content, still at English URLs and with the English header and sidebar around it. List several locales to try them in order: `{ 'zh-tw': ['zh', 'en'] }`.

When a page shows another locale’s content, its `isFallback` is `true` and `contentLocale` is the locale of the content. Use them to tell readers and to point search engines to the original:

```astro
---
const { id, isFallback, contentLocale } = Astro.props;
---

{isFallback && <link rel="canonical" href={toHref('docs', id, contentLocale)} />}
{isFallback && (
  <Callout.Root>
    This page is not translated yet. <a href={toHref('docs', id, contentLocale)}>View the original</a>
  </Callout.Root>
)}
```

Without `fallback`, an untranslated page does not exist in that locale. `dir()` skips it, and `page()` referring to it fails the build.

## Language switcher

Each page’s `alternates` lists every locale it exists in with its URL, including the current one:

```astro
---
const { locale, alternates } = Astro.props;
---

<nav aria-label="Language">
  {alternates.map((alt) => (
    <a href={alt.href} hreflang={alt.locale} aria-current={alt.locale === locale ? 'true' : undefined}>
      {alt.locale}
    </a>
  ))}
</nav>
```

The same data fits in `<head>`: `<link rel="alternate" hreflang={alt.locale} href={alt.href} />`.

## The site’s own pages

Pages that do not come from a collection, such as a home page, also go under `[...locale]`, with `toLocaleParam` generating one for each locale:

```astro
---
// src/pages/[...locale]/index.astro
import { type Locale, toLocaleParam } from '@minidocs/astro/runtime';

const locales: Locale[] = ['zh', 'en'];

export function getStaticPaths() {
  return locales.map((locale) => ({ params: { locale: toLocaleParam(locale) }, props: { locale } }));
}
---
```

When the default locale has no locale segment, `toLocaleParam` returns `undefined` for it, which generates `/`.

Put the 404 page at `src/pages/404.astro`, not under `[...locale]`, since hosting platforms usually only look for `404.html` at the root of the site.

## Links in content

A link to a file in the same locale directory stays in that locale, untranslated pages included, see [Links in Content](/en/docs/guides/links.md#multilingual-sites).
