Internationalization
Overview
The app is fully internationalized with next-intl. It ships with en and
es; every page lives under src/app/[locale]/ and is rendered in the reader's
language. Routing and locales are defined in src/i18n/routing.ts.
Messages
UI strings live in messages/en.json and messages/es.json, grouped by
namespace. Never hardcode user-visible text:
// Client component
const t = useTranslations("marketing")
return <h1>{t("home.headline")}</h1>
// Server component
const t = await getTranslations("marketing")
Add every new key to both en.json and es.json in the same change. A test
(messages-parity.test.ts) fails the build if the two files drift out of sync,
so missing translations can't slip through.
Navigation
Always use the navigation helpers from @/i18n/navigation — Link, redirect,
useRouter, usePathname — never the raw Next.js equivalents. They keep the
active locale in the URL automatically:
import { Link } from "@/i18n/navigation"
<Link href="/pricing">…</Link> // resolves to /en/pricing or /es/pricing
Formatting
Format dates, numbers, and currencies with next-intl's formatter, not
toLocaleDateString or Intl directly:
const format = useFormatter()
format.dateTime(date, { dateStyle: "long" })
The user's language drives everything
A signed-in user's saved locale preference drives the entire UI — panel and
public pages alike — and their emails. The preference wins over the URL locale;
the proxy (src/proxy.ts) redirects to it. A public language switcher persists
the choice before navigating.
Localized errors and emails
- tRPC errors carry translation keys, not English strings (see The tRPC API), so the UI translates them.
- Email templates accept a
localeprop and render in the recipient's language (see Email).
RTL-readiness
Use CSS logical properties (ps/pe, ms/me, border-s) rather than
directional ones (pl/pr) in your styling layer, so a right-to-left locale
lays out correctly if you add one.
Adding a locale
Add the locale to src/i18n/routing.ts, create its messages/<locale>.json
(keys matching the others), and — for these docs — a
src/content/docs/<locale>/ folder. Missing doc pages fall back to the default
locale automatically.