Adding a third locale broke things the build could never catch
I added Portuguese (Brazil) as a third locale on this site, mirroring the existing Spanish setup. I figured it would be mechanical: copy the es/ pattern, translate the strings, done. Instead it surfaced two bugs that had been sitting quietly in a two-locale system the whole time, invisible until a third locale exposed the assumption.
The site only ever had two locales
The i18n code wasn’t built generically for N locales. It was built for exactly two, and it showed the moment I tried to add a third. The locale switcher was a binary ternary - “show the other one,” not “loop over every locale except the current one.” A type guard checked value === 'en' || value === 'es' instead of checking membership in a list. And TypeScript’s Record<UiLocale, string> types meant that the moment pt joined the union, every single UI string needed a pt translation or the build wouldn’t typecheck.
That last one is actually a feature: it meant I couldn’t ship a half-translated page by accident, the same guardrail that caught a paragraph skipping translation earlier. But it also meant “just translate the Deep Cut Atlas page” stopped being an option once pt existed anywhere in the type system.
None of this was a bug with two locales. Binary code works fine for two things. It just doesn’t generalize, and nothing forces you to notice until you add a third.
The build error that was about a different package
I added pt to Astro’s i18n.fallback map, so unbuilt pt pages would gracefully render English content the way Spanish does, and the build failed with a static path error inside the shared blog package’s route files. Not my code.
Astro’s fallback rendering eagerly prerenders every dynamic route under every fallback locale, third-party integrations included. The blog package’s own locale type is hardcoded to English and Spanish only, and it choked trying to resolve a Portuguese path for a route it doesn’t know how to translate.
The fix was leaving pt out of the fallback map entirely: unbuilt pt pages 404 instead of silently rewriting to English. That traded one problem for a smaller one. A page that had been relying on the old fallback to serve English content at a pt URL now genuinely 404’d, so I had to go write a real Portuguese translation for it instead of letting it ride the safety net.
The bug the build could never have caught
Once everything built and every page I’d touched rendered correctly, I ran a review pass over the diff before calling it done. It caught something I’d missed: every page on the site, including pages with no Portuguese version at all, was now advertising an SEO alternate-language link claiming a Portuguese translation existed at a URL that 404s.
The mechanism: the site emits one hreflang tag per configured locale, on every page, unconditionally. Harmless with Spanish, because Spanish has a site-wide fallback and every URL under /es/ resolves to something even if it’s English content at a Spanish URL. Portuguese has no such fallback - that’s the fix from the previous section - so the moment pt joined the locale list, every page without a real Portuguese counterpart started pointing search engines at a dead link.
Nothing in the pipeline would catch this. The build succeeds either way; an hreflang tag pointing at a 404 isn’t a build error. The accessibility linter only checks pages that exist, so it never loads a page that doesn’t and never notices what’s claimed about it. And the manual page-by-page browser check I’d already done only looked at pages I’d actually built - by construction I hadn’t loaded a page that’s supposed to not have a Portuguese version to see what it says about itself.
The fix was making the set of locales a page advertises an explicit, opt-in property of that page, defaulting to the two locales that have always had full coverage, rather than an unconditional per-locale loop.
Takeaways
Code written for exactly two of something is a time bomb for whenever a third shows up: binary ternaries, hardcoded two-value type guards, anything that reads “the other one” instead of “everything except this one.”
A green build means the code you wrote is internally consistent, not that it’s correct. Metadata claims - SEO tags, sitemaps, anything that describes content rather than rendering it - can be wrong in ways nothing in a normal pipeline checks, because nothing renders the absence of a page and inspects what it says about itself. A pass specifically asking “what would only show up by NOT loading a page” is worth doing before calling an i18n change complete.
Related reading
Two Astro i18n bugs that don't show up until you diff the rendered HTML
Astro's fallback rewrite silently shadows real translations behind getStaticPaths, and Astro.url lies about where a fallback-rendered page lives - both invisible in the browser.
Fixing four small lies on the Deep Cut Atlas page
A dash that didn't match the app's own typography, a tagline that didn't parse, a feature claim the app can't back up, and one missing trailing slash in the structured data.
Translating a marketing page without duplicating it
The Spanish page existed, had a URL, showed up in hreflang - and every word was English. The fix that almost shipped was 130 duplicated lines waiting to drift.