minidocs

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

// 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:

---
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:

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:

---
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:

---
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:

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

A link to a file in the same locale directory stays in that locale, untranslated pages included, see Links in Content.

On this page
Search in
Type to start searching