Supported Locales Config

config/supported_locales.ts is the central locale registry. Every locale-aware subsystem — routing, translation loading, template-variant resolution, sitemap generation, hreflang clusters, RSS feeds, the locale picker, and the SSG page tree — reads its locale list from this one file. Add a locale here and it propagates everywhere automatically.

Configuration File

export const locales = ["en-us", "sl-si"] as const;

export const active_locales = ["sl-si", "en-us"] as const;

export const soft_launch_locales: string[] = [];

export const default_locale = "sl-si";

export const locale_names: Record<string, string> = {
  "en-us": "English",
  "sl-si": "Slovenian",
};

export const locale_aliases: Record<string, string> = {};

Every export is consumed at import time by src/lib/locale.ts, which validates the configuration and throws immediately on any inconsistency — a malformed locale, a missing locale, an alias chain, or an aliased default all fail at startup rather than surfacing as a confusing runtime gap.

locales

export const locales = ["en-us", "sl-si"] as const;

The complete list of locales the project supports. Every locale here must have at least one translation JSON file somewhere under src/public/ (e.g. en-us.json, sl-si.json). The translation loader (lib/i18n.ts) walks the public directory collecting <locale>.json files, keying each tree by the locale names listed here.

locales is the master set. All other exports (active_locales, default_locale, locale_aliases keys/targets) must be drawn from this list. The validation in src/lib/locale.ts enforces this at import time.

ConstraintEnforced
Must not be empty✅ Import-time throw
Every entry must match [a-z]{2,3}(-[a-z]{2,3})* (canonical lowercase BCP 47)✅ Import-time throw
No duplicates✅ Import-time throw
Must contain default_locale✅ Import-time throw
Must contain every active_locales entry✅ Import-time throw
Must contain every alias key and target✅ Import-time throw

active_locales

export const active_locales = ["sl-si", "en-us"] as const;

The subset of locales that is publicly surfaced. Controls three things:

  1. Which pages the SSG builds. Only locales in active_locales get their own rendered page tree in dist/. A locale in locales but not active_locales has its translation files loaded but produces no output.
  2. Which locales appear in the locale picker. props.active_locales is pre-populated from this array in every page render.
  3. Which locales get sitemap <url> entries and hreflang alternates.

Usually active_locales equals locales. Narrow it when a locale is still being translated — the translation files exist so the loader finds them, but the locale isn't published yet.

soft_launch_locales

export const soft_launch_locales: string[] = [];

Locales that are built and reachable by URL, but hidden from discovery. This is the per-locale counterpart of a draft page (see Content Visibility). A soft-launched locale:

  • Is fully rendered by the SSG (pages exist at their locale-prefixed URLs)
  • Is excluded from the sitemap
  • Is excluded from RSS/JSON feeds
  • Is dropped from hreflang clusters (search engines won't index it)
  • Is excluded from the locale picker
  • Gets <meta name="robots" content="noindex"> on every page

Use this to publish a translation for stakeholder review before announcing it publicly. When ready, move the locale from soft_launch_locales to active_locales.

Unlike draft: true frontmatter (which applies per-page), soft_launch_locales applies per-locale — every page in that locale is affected.

default_locale

export const default_locale = "sl-si";

The locale served at the site root with no URL prefix. All other locales get a locale-prefixed URL path (e.g. /en-us/about/).

RoleBehaviour
Root URL/ renders the default locale's homepage
Template resolution fallbackWhen a locale-specific template variant is missing, the default locale's variant is tried next
Markdown resolution fallbackSame chain for .md files
Sitemap x-default hreflangThe default locale's URL is the x-default alternate
RSS/JSON feed defaultThe default locale's feed is at the blog root (no prefix)

Must be in locales and must not be aliased (the default locale must own its translation files, not borrow them).

locale_names

export const locale_names: Record<string, string> = {
  "en-us": "English",
  "sl-si": "Slovenian",
};

Human-readable labels for the locale picker. Each key is a locale from locales; each value is the display name shown in the picker UI. These names are NOT translated — they're the language name in its own language (so the Slovenian entry reads "Slovenščina", not "Slovenian"), sourced from the ui.language_names translation key.

Pre-populated into every page render as props.locale_self_names so the picker needs no per-page data entry.

locale_aliases

export const locale_aliases: Record<string, string> = {};

UI-string sharing between locales. When a locale (the key) should use another locale's (the value) translation strings instead of its own .json files:

export const locale_aliases: Record<string, string> = {
  "de-at": "de-de",   // Austrian German shares German translations
};
AspectBehaviour
Routing & URLsThe key locale gets its own URL prefix (/de-at/)
SSG buildThe key locale gets its own rendered pages
Translation filesThe key locale does NOT need its own .json files
UI stringsRequests for the key locale resolve to the target's translations
ValidationOne level only — targets must not themselves be aliased

Constraints:

  • Both key and target must be in locales
  • A locale cannot alias itself
  • Targets must not be aliased (no chains like "de-at" → "de-de" → "de")
  • default_locale must not be aliased

The resolution happens at render time via resolve_ui_locale() in src/lib/locale.ts, which the static site generator and dev server both call.

The Override File

config/supported_locales.override.ts exists alongside the main config for the release pipeline. When the starter is cloned, the override file ships with a single-locale default ("en-us"), and the main config ships with the author's locales ("en-us", "sl-si").

The @release-sync-hash comment at the top is a content hash used by the release script to determine whether the override needs updating. It is not read at runtime — only the main config file is imported directly.

When you create your own project, you are expected to edit config/supported_locales.ts directly. The override file exists purely to let the starter ship sensible defaults for new projects while preserving the author's own configuration.

Locale Helper Functions

src/lib/locale.ts imports this config and exports the following functions. They are the canonical locale utilities — any code that manipulates locale codes should use these rather than inlining string manipulation.

FunctionInputOutputDescription
locale_language(locale)"de-at""de"Short language subtag (for <html lang="...">)
locale_region(locale)"de-at""at"Region subtag after the first hyphen
locale_url_segment(locale)"de-at""de-at"URL/filename form — identity, since locale is already canonical lowercase
format_bcp47(locale)"de-at""de-AT"Conventional mixed-case BCP 47 (for hreflang, og:locale)
canonical_locale(value)"DE-AT""de-at"Case-insensitive match against locales; returns canonical form or null
resolve_ui_locale(locale)"de-at""de-de"Resolves via locale_aliases; returns the target locale for UI strings
unaliased_locales()["de-de"]All locales that own translation files (not alias keys)

Validation

assert_valid_locale_config() runs once at first import and validates:

  • locales is non-empty
  • Every locale matches the canonical lowercase BCP 47 shape
  • No duplicates in locales
  • default_locale is in locales
  • Every entry in active_locales is in locales
  • default_locale is not aliased
  • Alias keys and targets are all in locales
  • No alias points at itself
  • No alias chains (target must not be an alias key)

Any violation throws immediately at startup with a descriptive message.

How It's Consumed

config/supported_locales.ts          ← single source of truth
         │
         ├── src/lib/locale.ts       ← validates + exports helpers
         │
         ├── lib/i18n.ts             ← loads <locale>.json files for each locale in `locales`
         │
         ├── lib/static_site.ts      ← build_static_route_map() — canonical → per-locale URL lookup
         │
         ├── scripts/ssg/pipeline.ts ← builds one page tree per active_locale
         │
         ├── scripts/dev/site_state.ts ← resolves locale on incoming request
         │
         ├── scripts/generate_sitemap.ts ← one <url> per (canonical × active_locale)
         │
         ├── scripts/generate_rss.ts  ← per-locale feeds for default + each non-default active_locale
         │
         ├── scripts/generate_search_index.ts ← per-locale search indexes
         │
         └── scripts/shared/page_data.ts ← injects props.locale, props.active_locales, props.html_lang

Relationship to the i18n System

This config is the data side of the locale system; the i18n pages cover the usage side:

This Reference Coversi18n Pages Cover
What each export is, its type and constraintsHow to add a locale, create a picker, format dates
Validation rules and failure modesTranslation file structure and key naming
Alias mechanics and resolution chainlocalized_path_for_locale() usage
The override-file release mechanismLocale-variant template resolution
Helper function signaturesprops.locale and props.html_lang in templates

See Languages & Locales for the how-to guide, and Translations for the translation file format and loading.