Thin Route + Shared Component: A Pattern for Bilingual Pages in Astro
A bilingual site can easily end up with markup duplicated twice — once in /page.astro, once again in /en/page.astro — where every design change has to be remembered and applied in two places. This site uses a different pattern: thin route + shared component.
How it works: every listing/detail page (Home, Portfolio, Blog) has one locale-agnostic “body” component under src/components/pages/, for example BlogIndex.astro. That component takes locale as a prop, pulls the dictionary via getDictionary(locale) for all the chrome text (labels, section titles), and calls getBlogPosts(locale) for the content data — nothing is hardcoded.
The route file itself ends up nearly empty — src/pages/blog/index.astro just imports the component and renders <BlogIndex locale="id" />, while src/pages/en/blog/index.astro renders <BlogIndex locale="en" />. One line differs by locale, everything else is identical.
Why doesn’t every page use this pattern? Pages like About and Contact are deliberately excluded, because their content is prose that doesn’t repeat — my bio on the About page doesn’t have the same repeating structure as dictionary chrome, so it makes more sense to write it directly per file per language than to force it into a dictionary system designed for short, repeated labels.
The real payoff: if I change the blog post card layout, I only need to edit one file (PostCard.astro, called from BlogIndex.astro), and it stays consistent across both languages automatically — no need to remember to apply the same change twice. If the markup were duplicated from the start, I’d have to discipline myself to edit two places every time, and that’s exactly the kind of discipline that’s easy to drop when you’re in a hurry.
This pattern isn’t clever, just a consistent separation of concerns. But small discipline like this is what keeps a bilingual site maintainable by one person.