---
title: Internationalization (i18n)
order: 8
icon: phosphor-duotone:translate
tags: [guides, i18n]
---

# Internationalization (i18n)

Translate your docs into other languages, each getting its own URL prefix,
its own `<html lang dir>`, and an automatic language switcher - no plugin,
no separate build step.

## Adding a locale

Translated content lives in `docs/i18n/<code>/`, mirroring your regular
`docs/` tree page-for-page:

```text title="Project structure"
docs/
├── index.md
├── guides/
│   └── setup.md
└── i18n/
    ├── es/
    │   ├── index.md
    │   └── guides/
    │       └── setup.md
    └── ar/
        └── index.md
```

`<code>` becomes both the folder name and the built URL prefix
(`docs/i18n/es/guides/setup.md` → `/es/guides/setup/`), so keep it short -
a bare language code (`es`, `fr`) or a language-region pair (`pt-BR`,
`zh-Hans`) both work, letters/digits/hyphens only. Your regular `docs/`
tree is always the **default locale**, built unprefixed at the site root
exactly as it is today - adding `docs/i18n/` doesn't change anything about
it.

Give each locale a display label (and, for a right-to-left language, its
own direction) in `bxsites.json`:

```json title="bxsites.json" linenums="1"
{
	"i18n": {
		"defaultLocale": { "code": "en", "label": "English" },
		"locales": [
			{ "code": "es", "label": "Español" },
			{ "code": "ar", "label": "العربية", "dir": "rtl" },
			{ "code": "pt-BR", "label": "Português (Brasil)", "flag": "🇧🇷" }
		]
	}
}
```

`defaultLocale` only needs setting if your default locale isn't English;
`locales` is the list of everything else. Every `docs/i18n/<code>/`
folder builds automatically once it exists - `locales` just supplies its
display label and text direction. A folder with no matching `locales`
entry still builds (using its bare code as its own label), so this is
metadata, not what turns the feature on or off.

`flag` is optional - the switcher already picks a sensible flag emoji for
~40 common language codes on its own (checking a region code like `pt-BR`
first, then falling back to the base language `pt`). Set `flag` yourself
only to override that guess, or for a code the built-in lookup doesn't
recognize (it falls back to a plain 🌐 in that case).

## What gets built

Each locale is a real, fully independent build - its own `search-index.json`,
its own `assets/`, everything a normal build produces - written under
`site/<code>/` (`site/es/`, `site/ar/`). Nothing needs enabling per-locale:
once `docs/i18n/es/` exists, `bxSites build` picks it up on its own.

## Untranslated pages

A locale doesn't need every page translated before it's usable. A page
missing from `docs/i18n/es/` still builds at its expected URL - showing
the default locale's own content, with a small notice at the top of the
page saying it hasn't been translated yet. Nothing 404s, nothing looks
half-built while a translation is in progress.

Every locale's nav is always the exact same shape as the default locale's
own - same pages, same order, same nesting (whatever `docs/`'s own folder
structure, or an explicit [`nav`](../configuration.md#nav), already
produces) - just with each page's title/content swapped in from its own
translation where one exists. This is also what makes the language
switcher work: switching languages lands you on the *same page*, translated
or not, never that locale's homepage.

## The language switcher

Once more than one locale exists, every theme renders a flag-icon language
dropdown in the header automatically - nothing to opt into, same as the
[version switcher](../configuration.md#versioning). It shows the current
locale's flag as a trigger; opening it lists every locale with its own
flag and label, the current one marked active. Pick a locale you're not
currently building and it simply won't render at all.

## Versioned and translated docs

See [Versioning](versioning.md) for `docs/versions/<name>/` itself.
Versions and locales compose one level: put a `docs/versions/<name>/i18n/<code>/`
folder next to a version's own pages, mirroring that version's own
structure exactly the same way a top-level `docs/i18n/<code>/` mirrors
`docs/` itself:

```text title="docs/versions/2.0/ layout"
docs/
  versions/
    2.0/
      index.md
      guides/
        setup.md
      i18n/
        es/
          index.md          # translated
          guides/
            setup.md        # untranslated pages still fall back, same as top-level i18n
```

This builds `site/versions/2.0/es/`. A version's own default-locale pages
(`site/versions/2.0/`) get a language switcher too, listing only the
locales *that version itself* has translations for - a version with no
`i18n/` subfolder of its own renders exactly as it did before this
existed, no switcher shown. Switching version always drops back to that
version's own default locale (never assumes the target version has the
same translation); switching locale always stays on the current version.

## What's out of scope (for now)

- **Theme chrome stays in English.** "Edit this page," "Last updated,"
  the search placeholder, and similar UI strings aren't translated per
  locale yet - only your own page content is. A locale's actual reading
  experience is fully translated; the surrounding theme furniture isn't.
- **RTL layout mirroring is baseline, not pixel-perfect.** `dir="rtl"` is
  set correctly, and the sidebar/header genuinely mirror, but a few
  decorative details (an admonition's accent-bar side, for instance)
  don't flip yet.
- **No automated translation.** Every `docs/i18n/<code>/` file is
  authored by hand, the same as any other markdown page - there's no
  machine-translation step.

## Custom icons and includes

A `custom:` icon reference and a `::: include` both resolve against your
project's own `docs/assets/`/reusable-content, regardless of which locale
is being built - these are shared assets, not something a translator needs
to duplicate per locale.

## SEO

Every locale's pages are included in `sitemap.xml` and `llms.txt`
alongside the default locale's own, the same way [versioned](../configuration.md#versioning)
pages are.
