The architecture
/en/blog/hello ← default locale, still prefixed (cleaner than a special case)
/ru/blog/hello
/zh/blog/hello
/ja/blog/hello
/de/blog/hello
Three moving parts:
- Dictionaries — one JSON per locale, identical shape, typed as
Record<Locale, typeof en>so TypeScript screams when a key drifts. - Dynamic routes —
src/pages/[locale]/...withgetStaticPaths()returning every locale. - Content fallback — write posts once in
en/, render them under every locale until translations exist.
The type trick that saves you
export type Locale = "en" | "ru" | "zh" | "ja" | "de";
const dictionaries: Record<Locale, typeof en> = { en, ru, zh, ja, de };
typeof en is the schema. Add a key to en.json and the build fails until all four other dictionaries catch up.
Don’t forget the head
<link rel="alternate" hreflang="..."> for every locale plus x-default, canonical per page, and Astro.url-derived paths. Search engines reward the boring plumbing.