My i18n Setup Was Right. Until It Wasn't.
Best practices have an expiry date. They expire silently.
I'm building a 4-language landing page for a client — Catalan, Spanish,
English, French. Before I wrote a single page, I asked Claude what the
cleanest way to do i18n in Astro was. The answer was solid. I wrote it into
the specs. The scaffold worked.
Until I added a second page. This is the story of how I almost shipped a
maintenance nightmare without ever breaking a single rule from my own
specs.
The setup
The original recommendation was straightforward: one file per language.
Astro had recently stabilized native i18n support — the i18n: {
defaultLocale, locales, routing } block in astro.config.mjs — and the
simplest pattern paired that config with a directory tree like:
src/pages/
├── index.astro # Catalan (default)
├── es/index.astro # Spanish
├── en/index.astro # English
└── fr/index.astro # French
Four files. One per language. Each file pulled its strings from a shared
src/i18n/ui.ts catalogue, so the content was DRY even if the structure
wasn't. This worked. It went into the specs as the canonical pattern.
The growth
Months in, the project needed two more pages: /gallery and /press. I did
what any disciplined builder does — I followed the existing pattern. Two
new pages. Eight new files.
Something started to feel off, but I couldn't name it. The specs said "one
file per language." I was following the specs.
The catch
It clicked during a review with Claude. I asked, almost as a sanity check:
"we made four versions with the text hardcoded in each language?" — and as
I typed the question, I heard how absurd it sounded.
We had four copies of every page. The structure was duplicated. The text
was duplicated. Every bug would be quadrupled. And worse: when a file lives
in an es/ folder, the brain treats it as "the Spanish version" — so you
start writing Spanish strings directly into the file, bypassing the very
i18n catalogue you built. The pattern itself was inviting the regression.
That was the symptom. The cause was deeper: the per-language file pattern
scales the wrong axis. Add a new page and your surface multiplies by N
(where N is the number of languages). The structure and the language are no
longer orthogonal.
The refactor
Astro has a feature the original setup hadn't been using: dynamic routes
via [lang]. One file generates a route for each parameter, statically, at
build time:
src/pages/
├── index.astro # / (Catalan, default)
├── gallery.astro # /gallery (Catalan)
├── press.astro # /press (Catalan)
└── [lang]/
├── index.astro # /es, /en, /fr
├── gallery.astro # /es/gallery, /en/gallery, /fr/gallery
└── press.astro # /es/press, /en/press, /fr/press
Six files instead of thirteen. A 54% reduction in surface area. The
structure lives in one place per page. The language varies via the route
param. The catalogue handles the strings. Each axis is finally orthogonal.
The refactor itself took about an hour: getStaticPaths per file, swap
useTranslations('es') for useTranslations(Astro.params.lang), rewrite the
language switcher to do a URL-prefix swap.
Why this matters
I wasn't violating my specs. The specs were the trap. They encoded the
assumption that the project would stay at one or two pages — an assumption
that was true when I wrote them, and stopped being true the moment I added
the third.
Specs are snapshots. They capture what you knew when you wrote them. A spec
that says "follow pattern X" is silently dependent on the conditions that
made X a good idea in the first place. When those conditions change, the
rewrite the language switcher to do a URL-prefix swap.
Why this matters
I wasn't violating my specs. The specs were the trap. They encoded the assumption that the project would stay at one or two pages — an
assumption that was true when I wrote them, and stopped being true the moment I added the third.
Specs are snapshots. They capture what you knew when you wrote them. A spec that says "follow pattern X" is silently dependent on the
conditions that made X a good idea in the first place. When those conditions change, the spec stops protecting you and starts pushing
you toward the wrong answer.
The takeaway isn't "review your specs every time you add a feature." That's exhausting and nobody does it. The takeaway is to listen to
friction. When you find yourself hardcoding strings, or copy-pasting structure, or saying "this is the third time I'm doing this" —
that's the signal that a pattern is past its expiry date.
Takeaway
Best practices have an expiry date. They expire silently.
The agent writes the code, the spec sets the pattern, but you're still the engineer. The friction in your hands is the only thing that
knows when something used to be right and isn't anymore.
Top comments (0)