This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy under /_llms/. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content Skip to navigation
Getting Started

Add a second locale to your site

Turn a single-language DocSite into a bilingual one by registering a second locale, translating three pages, and letting the built-in LanguageSwitcher appear in the header.

By the end of this tutorial you'll have a running DocSite at http://localhost:5000 that serves three English pages at /, /about, and /getting-started, plus three Spanish translations at /es/, /es/about, and /es/getting-started. A LanguageSwitcher component appears in the header and toggles between the two languages without any manual layout edits.

A single ConfigureLocalization action on DocSiteOptions enables multi-locale behavior. The default locale lives at the URL root; every other locale gets a folder prefix equal to its code. The LanguageSwitcher is already wired into DocSite chrome and stays hidden until a second locale is registered.

Prerequisites

You'll work in the DocSite project from Scaffold a documentation site with DocSite. The finished version of every change lives in examples/BeyondLocaleExample — including the Spanish translations you'll author in section 3 — as a reference to check against.


1. Confirm the single-locale baseline

Your scaffold host serves markdown from Content/ with no localization and no switcher. A clear baseline makes the contrast obvious when localization arrives in section 2.

There is no ConfigureLocalization action on DocSiteOptions yet, so LocalizationOptions.IsMultiLocale is false and the built-in LanguageSwitcher in MainLayout.razor renders nothing. The host you carry forward looks like this — the same AddDocSite shape from the scaffold tutorial:

csharp
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.AddDocSite(() => new DocSiteOptions
{
    SiteTitle = "Beyond Locale",
    SiteDescription = "Adding a second locale to a Pennington DocSite.",
    GitHubUrl = "https://github.com/usepennington/pennington",
    HeaderContent = """<a href="/">Beyond Locale</a>""",
    FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">Built with Pennington DocSite.</footer>""",
});
  
var app = builder.Build();
  
app.UseDocSite();
  
await app.RunDocSiteAsync(args);
1

Place three English pages directly under Content/

Add index.md, about.md, and getting-started.md directly under Content/ — not in any locale subfolder. These are the default-locale pages, and they own the URL root.

markdown
---
title: Welcome
description: A DocSite homepage teaching Pennington localization.
order: 10
---
  
This site is written in two languages. The English version you're reading
lives under `Content/` — the default locale owns the URL root so its pages
serve from `/`, `/about`, and `/getting-started`.
  
Use the language switcher in the site header to jump to the Spanish version.
Every URL on this site has an equivalent in each configured locale, and the
`LanguageSwitcher` component in `MainLayout.razor` builds those links
automatically from the current request path.
markdown
---
title: About
description: About this localized DocSite example.
order: 20
---
  
This is a minimal DocSite that demonstrates **locale-aware URLs**. Every
markdown file under `Content/` is the English (default) version. Every
matching file under `Content/es/` is the Spanish translation.
  
When a visitor navigates to `/es/about`, `LocaleDetectionMiddleware` strips
the `/es` prefix, stores `"es"` in `LocaleContext`, and the DocSite's
`DocSiteContentResolver` picks up the Spanish markdown from `Content/es/about.md`.
If a Spanish file is missing, the resolver falls back to the English copy
and marks the page as a translation-fallback so the reader knows.
markdown
---
title: Getting Started
description: Get started with the localized DocSite example.
order: 30
---
  
To add a new locale to your own Pennington site:
  
1. Open `Program.cs` and call `loc.AddLocale(code, new LocaleInfo(displayName))`
   inside the `ConfigureLocalization` action on `DocSiteOptions`.
2. Create `Content/<code>/` and copy each page you want translated from the
   default-locale tree, translating the front matter `title:` and the body.
3. Run `dotnet run``LanguageSwitcher` appears in the site header as soon
   as `LocalizationOptions.Locales.Count > 1`.
  
There is no other wiring. The default locale keeps its URLs unchanged; every
additional locale gets a URL prefix equal to its code.

Checkpoint

  • Run dotnet run from your project folder
  • Visit http://localhost:5000/, http://localhost:5000/about, and http://localhost:5000/getting-started — each English page renders
  • The DocSite header shows the site title and GitHub link but no language switcher pill — because only one locale is registered

2. Register a second locale with ConfigureLocalization

Add a ConfigureLocalization action that names "en" as the default and registers "es" as a second locale. Once LocalizationOptions.IsMultiLocale is true, the switcher, the locale detection middleware, and the per-locale search index all activate. UseDocSite already wires the locale-routing middleware internally — no extra app.Use… call.

1

Add the ConfigureLocalization action to your existing DocSiteOptions

The snippet below is your host with the change applied — the highlighted lines are the only additions. Add the ConfigureLocalization property inside the DocSiteOptions you already pass to AddDocSite, alongside the SiteTitle, GitHubUrl, and the rest, not replacing them. The using Pennington.Localization; directive at the top brings LocaleInfo into scope.

csharp
using Pennington.DocSite;
using Pennington.Localization;
  
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.AddDocSite(() => new DocSiteOptions
{
    SiteTitle = "Beyond Locale",
    SiteDescription = "Adding a second locale to a Pennington DocSite.",
    GitHubUrl = "https://github.com/usepennington/pennington",
    HeaderContent = """<a href="/">Beyond Locale</a>""",
    FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">Built with Pennington DocSite.</footer>""",
  
    ConfigureLocalization = loc => 
    { 
        loc.DefaultLocale = "en"; 
        loc.AddLocale("en", new LocaleInfo("English")); 
        loc.AddLocale("es", new LocaleInfo("Español", HtmlLang: "es")); 
    }, 
});
  
var app = builder.Build();
  
app.UseDocSite();
  
await app.RunDocSiteAsync(args);

The new action has three pieces:

  • DefaultLocale = "en" — English owns the URL root with no prefix.
  • AddLocale("en", new LocaleInfo("English")) — registers English with the display name the switcher shows.
  • AddLocale("es", new LocaleInfo("Español", HtmlLang: "es")) — registers Spanish. HtmlLang is what Pennington emits on the <html> element for that locale's pages.

AddLocale is overloaded with a string-only display-name shorthand; the localization how-to surveys the full LocalizationOptions surface.

Checkpoint

  • Rebuild and run the site (or let hot reload pick up the change)
  • Refresh http://localhost:5000/ — the DocSite header now shows a language switcher pill offering English and Español
  • Click Español — the URL becomes http://localhost:5000/es/ and you see a DocSite fallback notice explaining that Spanish content is missing, because no Content/es/ files exist yet

3. Add translated markdown under Content/es/

Now let's give Spanish its content. Mirror the three English pages under a Content/es/ subfolder — same file names, same front-matter keys, translated body copy. The content resolver matches each Spanish URL to the corresponding Spanish file.

1

Create Content/es/ and translate index.md

Create the Content/es/ subfolder and add index.md with Spanish front-matter and Spanish body copy. The rule that matters: the subfolder name matches the locale code passed to AddLocalees here, because that is the code registered in section 2. Files under Content/es/ serve from /es/*; files directly under Content/ serve from /*.

markdown
---
title: Bienvenido
description: Página de inicio de un DocSite que enseña la localización de Pennington.
order: 10
---
  
Este sitio está escrito en dos idiomas. La versión en español que estás
leyendo ahora vive en `Content/es/` — cada idioma adicional tiene su propia
subcarpeta en el árbol de contenido y un prefijo de URL igual a su código
(`/es/`, `/es/about`, `/es/getting-started`).
  
Usa el selector de idioma en la cabecera del sitio para volver al inglés.
El componente `LanguageSwitcher` en `MainLayout.razor` construye los
enlaces automáticamente a partir de la ruta de la solicitud actual.
2

Translate about.md and getting-started.md

Repeat the move for the two remaining pages. Each Spanish file keeps the same filename as its English sibling; URL routing derives the path from the filename, not from any front-matter key.

Skipping a translation is fine. The content resolver falls back to the default-locale copy and renders a FallbackNotice banner naming the requested and default locales.

markdown
---
title: Acerca de
description: Acerca de este ejemplo de DocSite localizado.
order: 20
---
  
Este es un DocSite mínimo que demuestra **URLs conscientes del idioma**.
Cada archivo markdown bajo `Content/` es la versión en inglés (el idioma
predeterminado). Cada archivo correspondiente bajo `Content/es/` es la
traducción al español.
  
Cuando un visitante navega a `/es/about`, el middleware
`LocaleDetectionMiddleware` elimina el prefijo `/es`, guarda `"es"` en
`LocaleContext`, y el `DocSiteContentResolver` del DocSite busca el markdown
en `Content/es/about.md`. Si falta un archivo en español, el resolvedor
recurre a la copia en inglés y marca la página como una traducción de
reserva para que el lector lo sepa.
markdown
---
title: Primeros Pasos
description: Primeros pasos con el ejemplo de DocSite localizado.
order: 30
---
  
Para añadir un nuevo idioma a tu propio sitio Pennington:
  
1. Abre `Program.cs` y llama a `loc.AddLocale(code, new LocaleInfo(displayName))`
   dentro de la acción `ConfigureLocalization` en `DocSiteOptions`.
2. Crea `Content/<code>/` y copia cada página que quieras traducir del
   árbol del idioma predeterminado, traduciendo el `title:` del front
   matter y el cuerpo.
3. Ejecuta `dotnet run``LanguageSwitcher` aparece en la cabecera del
   sitio tan pronto como `LocalizationOptions.Locales.Count > 1`.
  
No hay más cableado. El idioma predeterminado mantiene sus URLs sin cambios;
cada idioma adicional obtiene un prefijo de URL igual a su código.

Checkpoint

  • With the host still running, visit http://localhost:5000/es/ — the page renders in Spanish with no fallback banner
  • Visit http://localhost:5000/es/about and http://localhost:5000/es/getting-started — both serve Spanish translations
  • Inspect the <html> element in dev tools on a Spanish page — lang="es" (from the LocaleInfo.HtmlLang set in section 2)

4. Use the built-in LanguageSwitcher to move between locales

The LanguageSwitcher component is already included in DocSite's MainLayout.razor. Now let's verify that it swaps locales in place by rewriting the current URL, landing on the same page in the other language rather than bouncing back to the home page.

Navigate to http://localhost:5000/es/about, open the language switcher in the header, and click English. The URL becomes http://localhost:5000/about. The switcher strips the /es prefix because English is the default locale and preserves the rest of the path, so the About page stays in view.

Checkpoint

  • From http://localhost:5000/es/about, click English — the URL becomes http://localhost:5000/about
  • From http://localhost:5000/getting-started, click Español — the URL becomes http://localhost:5000/es/getting-started
  • From http://localhost:5000/, click Español — the URL becomes http://localhost:5000/es/ (the default locale's root maps to the secondary locale's prefix root)

Summary

  • A single-locale DocSite becomes multi-locale by adding one ConfigureLocalization action to DocSiteOptions — no explicit middleware call, no layout edits.
  • The default locale owns the URL root and every other locale gets a code prefix equal to the string passed to AddLocale, with the matching Content/<code>/ subfolder providing the translations.
  • The LanguageSwitcher appears automatically once LocalizationOptions.IsMultiLocale is true, and it rewrites the current URL in place rather than redirecting to the home page.
  • When a translation is missing, the content resolver falls back to the default-locale copy and renders a FallbackNotice banner naming the requested and default locales.