Localization

Ship docs in multiple languages with a sparse-overlay model — translate what you can, fall back automatically for the rest.


SharpDocs ships docs in multiple languages using an overlay model: a base file applies to every locale, and a .{locale}.{ext} file is a sparse override for that one locale. You translate only what you have time for; the rest falls back to the base. This applies uniformly to pages (*.md) and sidebar config (site.json).

Quick start

  1. Add defaultLocale and locales to sharpdocs.json:

    {
      "github": "https://github.com/you/my-library",
      "defaultLocale": "en",
      "locales": ["en", "vi"]
    }
    
  2. Move the sidebar into a base site.json next to it:

    {
      "title": "My library",
      "description": "A small library.",
      "sidebar": [ /* groups */ ]
    }
    
  3. (Optional) Add a sparse site.{locale}.json to translate any subset of labels:

    {
      "title": "My library (Tiếng Việt)",
      "sidebar": [
        { "label": "Bắt đầu", "items": [{ "label": "Cài đặt" }] }
      ]
    }
    
  4. (Optional) Add index.vi.md next to index.md to override that page in Vietnamese.

That's it. /, /en, /vi, /en/foo, /vi/foo all work.

File layout

{docsRoot}/
  sharpdocs.json              structural — locales, defaultLocale, github, artifacts
  site.json                   base — full sidebar with default-locale labels
  site.vi.json                overlay — sparse, optional
  index.md                    base page
  index.vi.md                 overlay page, optional
  get-started/
    install.md
    install.vi.md

In multi-project mode, each project directory has the same shape (its own sharpdocs.json + site.json + overlays).

Resolution rules

For any (slug, locale) request:

  1. If {slug}.{locale}.md exists → serve it.
  2. Else if {slug}.md exists → serve it (base fallback).
  3. Else → 404.

The locale suffix is detected only when the trailing dotted segment of the filename matches a locale declared in sharpdocs.json. So with locales: ["en", "vi"], the file install.foo.md is read as a base file with the literal slug install.foo (no locale match), not as a foo-locale override.

URL routing

Request Resolves to
/ 302 → /{defaultLocale}
/{locale} locale's index page
/{locale}/{slug} page overlay if present, else base
/{locale}/search?q=... search scoped to that locale
/{unknown-locale}/{slug} 302 → /{defaultLocale}/{slug} (URLs stay honest)

In multi-project mode the prefix is /docs/{projectId}/{locale}/... and /docs/{projectId} redirects into that project's default locale. Each project owns its own defaultLocale / locales list — they don't share.

When a requested locale isn't in the project's locales, SharpDocs redirects to the default locale (preserving the slug) rather than silently serving fallback content under the unsupported URL. URLs that exist are honest about which locale they're in.

site.{locale}.json is shaped exactly like site.json, but every field is optional. The merge is structural by position: each group/item in the overlay overrides the group/item at the same index in the base. Anything missing or empty inherits from the base.

So a 2-group overlay against a 5-group base translates groups 1-2 and falls back for groups 3-5. Same idea for items inside a group: ship 1 of 4 item labels and the other three keep their base text. slug is normally inherited from base too — translators only edit label.

Example: base ships 5 groups; the overlay translates only the first two:

{
  "title": "SharpDocs (Tiếng Việt)",
  "sidebar": [
    { "label": "Tổng quan", "items": [{ "label": "Giới thiệu" }] },
    { "label": "Bắt đầu",   "items": [{ "label": "Cài đặt" }] }
  ]
}

Failure semantics

Per-locale failures degrade only that locale. The project keeps its other locales.

  • Missing site.{locale}.json → silent, base used as-is for that locale.
  • Malformed overlay JSON → that locale is dropped with a logged error; other locales still load.
  • Overlay sidebar references a slug with no .md (base or override) → that locale is dropped.
  • defaultLocale itself fails to load → the whole project degrades.
  • No locales load successfully → the project degrades.

The base site.json is required. A missing base file is fatal for the project.

What about the language switcher?

_Layout.cshtml renders a dropdown automatically when the current project has 2+ locales loaded.

  • Native names. Each locale shows in its own language — "English", "Tiếng Việt", "日本語" — derived from CultureInfo.NativeName. The toggle button shows the current locale's native name next to a globe icon.
  • Slug-preserving. The switcher reads window.location.pathname at click time, so it preserves your current page across locales — including pages you reached via htmx navigation (where the navbar didn't re-render).
  • Persistent. Clicking the switcher writes a sharpdocs_locale cookie (1-year, SameSite=Lax). Subsequent visits to /, /docs/{id}, or the single-healthy-project shortcut land in your preferred locale if the project ships it; otherwise they fall back to that project's defaultLocale. Bare URL navigation does not update the cookie — a one-off shared link can't silently flip your preference.

The picker uses a globe icon rather than country flags by design: flags map to countries, not languages, and one flag for a multi-country language ("English" → US? UK? AU?) reads as a political statement to people from the others.