If you look at the address bar right now there is a language code in it. That is new. The whole blog now exists in English, Norwegian and Spanish: every post, every page, the navigation, the error messages, the dates. This was the biggest single change since the redesign, and it touched almost every file in the frontend plus the content model in the blog service.
The shape of it: every public URL carries a language prefix, /en, /no or /es. Visit without one and the server figures out where to send you, first from a cookie if you have picked a language before, then from your browser's Accept-Language, then English as the fallback:
const lang = resolveLanguage(
event.cookies.get(LANG_COOKIE),
event.request.headers.get('accept-language')
);
event.locals.lang = lang;
throw redirect(302, `${localizedPath(pathname, lang)}${event.url.search}`);Your explicit choice always beats the browser guess. Pick Spanish once and the blog stays Spanish until you say otherwise, no matter what your browser claims you prefer.
Content was the bigger question. Machine translating on the fly was never on the table (this is a self-hosted blog, nothing leaves the box), so each post is a real row per language, and the versions are linked in a translation group. Each version publishes on its own schedule, has its own body, and shares the slug with its siblings, so switching language on a post takes you to that same post, not to a translated front page. The switcher is inert when a translation does not exist yet, instead of dumping you on a 404.

Then came the interesting breakage. Moving every route under a language prefix silently broke something that had nothing to do with languages: the avatar upload limit. Caddy sits in front of everything with per-path body size limits, and its rule for /profile no longer matched because the profile page now lives at /en/profile. Uploads over the default cap started dying at the front door with an empty 413 instead of a friendly error. The fix is one matcher line covering all the prefixes, but the lesson is bigger: when you move every URL on the site, grep your reverse proxy config for paths too.
The review caught two more real ones. The tag list in the sidebar counted posts in all languages, so the Norwegian site showed tags that led to empty pages (the queries now filter by language). And the slug uniqueness check looked across all languages when it should only look within one, which would have blocked perfectly valid translations from saving.
Translating the interface was a few days of careful work with a 132-key message catalog that the type checker enforces, so a string missing in Spanish is a build error, not a blank button. Translating the content was honest manual labor: 34 posts and 3 pages, each rewritten twice. The archive pages, feeds and sitemap all speak per-language too, with hreflang links so search engines understand the versions belong together.
Velkommen, and bienvenidos. The switcher is in the rail if you want to see how a post about Norwegian islands reads in Norwegian.