Skip to content
Development

The homepage was the one page i18n forgot

By Victor Da Luz
astroi18ndev-logsite

The earlier i18n pass shipped the language switcher, hreflang, and Spanish blog routes. It never touched the homepage. Every visitor who flipped the switcher to Español landed on the exact same English hero, English project cards, English everything - just at a /es/ URL. The switcher worked. There was nothing on the other side of it.

This issue was the fix, and it turned into a decent tour of how much copy actually lives on a “simple” landing page.

The pattern was already there, I just hadn’t followed it everywhere

Header.astro and Footer.astro already had the right shape:

const locale = Astro.currentLocale ?? 'en';
const strings = ui(locale);

Pull the locale off the request, look up a string table, render. That’s it. But none of the six components that actually make up the homepage - Hero, SoftwareSection, GamesSection, MusicSection, GitHubWidget, ProjectCard - had ever been wired up that way. They were just… English literals, sitting in the markup, because nobody had asked them to be anything else yet.

So step one was mechanical: add a homepage key group to ui.ts, thread the same three lines into every component, swap literals for strings.whatever. Unremarkable work, and exactly the kind of thing that’s easy to half-do - miss one aria-label, one alt-text template, one button label - which is exactly what happened last time (the footer’s analytics paragraph got missed by the original pass entirely, fixed separately days before I picked this up). So I went component by component and listed every hardcoded string before touching any of them, instead of trusting a find-and-replace pass to catch them all.

The part that wasn’t just “add strings to ui.ts”

src/data/projects.ts is flat English - project taglines, descriptions, the game blurb, the music blurb, three shared-package pitches. No locale dimension anywhere. That data doesn’t belong in ui.ts (this is content, not UI chrome), so I turned each translatable field into a small locale-keyed record:

tagline: {
  en: 'Find the releases your favorite artists slipped out',
  es: 'Encuentra los lanzamientos que tus artistas favoritos sacaron sin que te dieras cuenta',
},

The one thing I didn’t split was the status enum ('In development' | 'Early development' | 'Live'). It has exactly one consumer in the whole codebase - a badge in ProjectCard - so instead of forking the type into an English-value/Spanish-value pair, I kept the enum as an internal identifier and added a translation map in ui.ts keyed by the enum values themselves. The data stays a stable identifier; only the label facing the visitor changes. Small decision, but it’s the kind of thing that’s annoying to unwind later if you guess wrong.

The dev server lied to me for twenty minutes

npm run dev gave me a 500 on /es/ - fine, expected, something’s probably wrong with my new page. Except the error was process is not defined, thrown from inside Astro’s own logger while it was trying to log a different error. And when I tested / - a page I hadn’t touched - it 500’d too. The dev server was broken for every route, not just mine.

Turns out astro dev runs through a Miniflare/workerd sandbox for the Cloudflare adapter, and that sandbox doesn’t have Node’s process global. Astro’s dev-mode error handler tries to log the real failure and crashes doing it, which buries whatever actually went wrong. The fix wasn’t a fix to my code at all - it was switching to npm run build && npm run preview, which runs the real built output through actual wrangler, not the dev shortcut. Worth a knowledge-base note so the next session doesn’t lose the same twenty minutes. (It bugged me enough that I went back later and dug out the real cause - that’s its own story.)

Results

Both routes verified in a real browser: hero, all four sections, both project cards, footer - fully Spanish on /es/, unchanged on /. Language switcher round-trips cleanly both directions. Project card “follow the dev log” links now go to /es/blog/project/... instead of dropping the visitor back into English mid-navigation - the one link-generation bug that would’ve been easy to miss since it doesn’t show up as untranslated text, just a wrong destination.

npm run a11y: 12/12 URLs pass, including the new /es/ route, 0 errors.

Related reading