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.
| Constraint | Enforced |
|---|---|
| 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:
- Which pages the SSG builds. Only locales in
active_localesget their own rendered page tree indist/. A locale inlocalesbut notactive_localeshas its translation files loaded but produces no output. - Which locales appear in the locale picker.
props.active_localesis pre-populated from this array in every page render. - 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/).
| Role | Behaviour |
|---|---|
| Root URL | / renders the default locale's homepage |
| Template resolution fallback | When a locale-specific template variant is missing, the default locale's variant is tried next |
| Markdown resolution fallback | Same chain for .md files |
Sitemap x-default hreflang | The default locale's URL is the x-default alternate |
| RSS/JSON feed default | The 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
};
| Aspect | Behaviour |
|---|---|
| Routing & URLs | The key locale gets its own URL prefix (/de-at/) |
| SSG build | The key locale gets its own rendered pages |
| Translation files | The key locale does NOT need its own .json files |
| UI strings | Requests for the key locale resolve to the target's translations |
| Validation | One 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_localemust 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.
| Function | Input | Output | Description |
|---|---|---|---|
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:
localesis non-empty- Every locale matches the canonical lowercase BCP 47 shape
- No duplicates in
locales default_localeis inlocales- Every entry in
active_localesis inlocales default_localeis 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 Covers | i18n Pages Cover |
|---|---|
| What each export is, its type and constraints | How to add a locale, create a picker, format dates |
| Validation rules and failure modes | Translation file structure and key naming |
| Alias mechanics and resolution chain | localized_path_for_locale() usage |
| The override-file release mechanism | Locale-variant template resolution |
| Helper function signatures | props.locale and props.html_lang in templates |
See Languages & Locales for the how-to guide, and Translations for the translation file format and loading.