---
title: Add a second locale to your site
description: "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."
canonical_url: https://usepennington.net/tutorials/beyond-basics/add-a-locale/
sidecar_url: https://usepennington.net/tutorials/beyond-basics/add-a-locale.md
content_hash: sha256:d11f230758a9e2ae122bed78502cd838004e57523f710372197356e2fdd39849
tokens: 3554
uid: tutorials.beyond-basics.add-a-locale
reading_time_minutes: 6
---

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](https://usepennington.net/reference/ui/utility.md) 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](https://usepennington.net/reference/ui/utility.md) is already wired into DocSite chrome and stays hidden until a second locale is registered.

 
## Prerequisites

 
 - .NET 10 SDK installed
 - Completed [Scaffold a documentation site with DocSite](https://usepennington.net/tutorials/docsite/scaffold.md) (provides the single-locale DocSite host this tutorial extends)
 - Completed [Add doc pages and link between them](https://usepennington.net/tutorials/docsite/first-doc-page.md) (so the front-matter shape of each page is already familiar)
 
 
You'll work in the DocSite project from [Scaffold a documentation site with DocSite](https://usepennington.net/tutorials/docsite/scaffold.md). The finished version of every change lives in [examples/BeyondLocaleExample](https://github.com/usepennington/pennington/tree/main/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:symbol,bodyonly
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);
```

 
**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:symbol
---
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:symbol
---
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:symbol
---
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](https://usepennington.net/reference/api/localization-options.md) 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.

 
**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:symbol,bodyonly,imports
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](https://usepennington.net/how-to/discovery/localization.md) 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.

 
**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 `AddLocale`** — `es` here, because that is the code registered in section 2. Files under `Content/es/` serve from `/es/*`; files directly under `Content/` serve from `/*`.

 
```markdown:symbol
---
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.
```

 
**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](https://usepennington.net/reference/ui/utility.md) banner naming the requested and default locales.

 
```markdown:symbol
---
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:symbol
---
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.
 
 
[Previous
                
                Add a hero, projects, and social links](https://usepennington.net/tutorials/blogsite/hero-projects-socials.md)[Next
                    
                Author a custom Razor component for markdown](https://usepennington.net/tutorials/beyond-basics/custom-razor-component.md)