---
title: "i18n Without Pain: Lessons From Running next-intl Across 9 Products"
description: "Internationalization isn't a translation file — it's an architectural decision. Locale-prefixed URLs, typed navigation, and conscious fallbacks."
author: "Anderson Henrique"
date: "2026-04-21T14:30:00Z"
updated: "2026-05-12T14:04:42.167481Z"
category: "engineering"
tags: ["i18n","next-intl","next.js","architecture","engineering"]
canonical: "https://www.ntlabs.dev/en/blog/i18n-without-pain-next-intl"
locale: "en"
---

When we started NTLabs, internationalization felt like a detail we'd address later. Just a JSON of translated keys and we'd be done. Within three months we learned that i18n isn't translation — it's architecture.

The first structural choice was to put the locale in the URL. /pt/blog and /en/blog instead of sniffing the language from a cookie or header. Sounds trivial, but it decides everything downstream: Google crawls them as two distinct pages with alternates, social sharing preserves the language, and edge caching works without header fragmentation.

The second was typed navigation. In next-intl, Link and useRouter come from @/i18n/navigation, not next/link. It feels bureaucratic until the day someone writes href="/blog" and the link silently breaks on /en because it didn't receive the prefix. With typed navigation, TypeScript refuses the shortcut.

The third was about fallback. When a key exists in pt but not in en, what happens? By default, next-intl breaks. We chose to keep it that way — we'd rather have a build error than a half-published post. This forces translation to be part of the definition of done, not an operational afterthought.

We now run nine bilingual products with the same pattern. Each carries its own namespaces (blog, careers, dedalo, agora), but they all share the same infrastructure: the middleware that detects locale, the [locale] route segment, the Supabase client receiving locale as a query parameter.

What changed in me was understanding that i18n is a cross-cutting concern — not a plugin you install. Every product decision carries a language decision with it, and the sooner that enters the conversation, the cheaper maintenance becomes.
