Adding a locale
Turn on a second language — the locales array, the parallel content and UI strings it expects, and what the LocaleSwitcher does with gaps.
i18n is wired from the start. The default siteConfig.locales: ['en'] keeps the
parallel routes dormant; adding a locale is a config edit plus the translated
files to back it.
- Add the locale to siteConfig
In
src/config/site.ts, add the code tolocalesand give it the per-locale metadata the config already keys by language:export const siteConfig = { defaultLocale: 'en', locales: ['en', 'es'], name: { en: 'Acme', es: 'Acme' }, description: { en: '…', es: '…' }, // hreflang already maps common codes: es → es-ES, etc. };astro.config.mjsreadsi18n.localesstraight from here — there’s nothing to change there. The default locale stays at/; the new one routes under/<lang>/. - Add the UI-string dictionary
Copy
src/i18n/en.jsontosrc/i18n/<locale>.jsonand translate the values. Keep exactly the same keys —en.jsondefines the dictionary shape, so a missing key is a type error and an extra one is ignored.cp src/i18n/en.json src/i18n/es.json # then translate the values in es.json - Translate the content
For every collection entry that should exist in the new locale, add a sibling under that locale’s folder, keeping the same slug:
src/content/blog/en/launch.mdx src/content/blog/es/launch.mdx ← same slug, translated body + frontmatterThe catch-all routes pick these up automatically —
/blog/launchand/es/blog/launchboth render. - Mirror any hand-written pages
Custom
.astropages need a[lang]/parallel (see Adding content). The shipped pages already have theirs, so they start working in the new locale as soon as it’s inlocales.
What the LocaleSwitcher does with gaps
You don’t have to translate everything at once. The LocaleSwitcher appears in
the chrome the moment there’s more than one locale, and the navigation degrades
gracefully:
- A sidebar / nav item whose slug has no entry in the current locale is hidden — so a Spanish reader never sees a link that would 404. An empty group disappears entirely.
- Internal links resolve through
getRelativeLocaleUrl(lang, path), so they always point at the right locale prefix.