Internationalization (i18n)

On this page

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:

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

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: 🇧🇷 }
{
	"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, 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. 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 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:

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.

Theme chrome (UI strings)

Every built-in theme's own surrounding UI text - the search placeholder, "On this page," "Edit this page," "Last updated," the 404 page, the tags page title, and the "hasn't been translated yet" notice itself - is translated per locale too, not just your own page content.

Four locales ship a built-in translation out of the box: de, es, it, and ja. Building any of those locale codes gets fully translated chrome automatically, nothing to configure. Any other locale code (or a page you'd rather word differently) falls back to English - override just the keys you care about with your own strings on that locale's own entry in bxsites.yaml:

i18n:
  defaultLocale: { code: en, label: English }
  locales:
    - code: fr
      label: Français
      strings:
        searchPlaceholder: Rechercher dans la documentation...
        onThisPage: Sur cette page
    - code: es
      label: Español
      strings: { toggleDarkMode: Cambiar a modo nocturno }
{
	"i18n": {
		"defaultLocale": { "code": "en", "label": "English" },
		"locales": [
			{
				"code": "fr",
				"label": "Français",
				"strings": {
					"searchPlaceholder": "Rechercher dans la documentation...",
					"onThisPage": "Sur cette page"
				}
			},
			{
				"code": "es",
				"label": "Español",
				"strings": { "toggleDarkMode": "Cambiar a modo nocturno" }
			}
		]
	}
}

A locale with a built-in translation (like es above) can still override individual keys the same way - your own strings always wins over both the built-in translation and the English default. defaultLocale itself can carry its own strings too, for a project whose default locale isn't English and wants its own chrome wording rather than falling back to the English default.

The full set of overridable keys: searchPlaceholder, searchAriaLabel, searchNoResults, toggleDarkMode, toggleNavigation, toggleSection ({title} placeholder), repository, version, language ({label} placeholder), onThisPage, editThisPage, downloadMarkdown, lastUpdated, notTranslatedNotice ({locale} placeholder), notFoundTitle, notFoundBody, tagsTitle, and pageNavigation.

What's out of scope (for now)

  • Blog chrome stays in English. The blog subsystem's own UI strings ("Categories," "Archive," "Read more," "N min read," ...) aren't covered by the strings override above yet - only the rest of the theme chrome is.
  • lastUpdated's own date value isn't locale-aware. Its label translates; the date/time value itself is still formatted the same way regardless of locale.
  • 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 pages are.

Edit this page Download Markdown Last updated Aug 28, 2026, 3:16:38 AM