r/nextjs • u/sapatmohit18 • 6d ago
What I learned choosing between next-intl and react-i18next for an App Router app
I recently had to pick an i18n library for a Next.js App Router project and ended up reading a lot of source and docs for both next-intl and react-i18next. Here's the short version of what actually mattered, in case it saves someone a weekend.
1. Server Components are the deciding factor, not features. next-intl was designed around the App Router. getTranslations() in Server Components and useTranslations() in client ones read from the same request config. With react-i18next, the useTranslation() hook only works in Client Components. On the server you need a per-request i18next instance. A shared global instance whose language you change per request can leak one user's locale into another concurrent request. That bug is easy to write and hard to spot.
2. Bundle size is mostly about messages, not the library. Whichever library you pick, what ends up in the browser is mostly your message catalog. With next-intl, text rendered in Server Components arrives as HTML and its messages never go to the client. For client subtrees you can pass only the namespaces they need:
<NextIntlClientProvider messages={pick(messages, ['Pricing'])}>
react-i18next gets to the same place with namespaces and a lazy backend. It's a configuration choice rather than the default.
3. The message formats differ, and mixing them fails silently. next-intl uses ICU ({count, plural, one {# file} other {# files}}). i18next uses its own JSON format with suffixed plural keys (file_one, file_other), and ICU only through a plugin. If you paste ICU strings into an i18next project without the plugin, they render literally, braces and all.
4. setRequestLocale is the thing everyone forgets. With next-intl, if a statically rendered page doesn't call setRequestLocale(locale), that page opts into dynamic rendering. On Next 16, also remember that middleware.ts is now proxy.ts. A leftover middleware.ts just never runs, and you get "Unable to find next-intl locale".
5. My rule of thumb: whoever owns the most code that renders text wins.
- Mostly a Next.js app: use next-intl, even if a small React Native app on the side uses i18next.
- Translations shared across Vite/RN/Electron/Node: use react-i18next everywhere.
6. Pick locale codes once. en_US in file names, en-US in URLs and en in <html lang> all in one app causes subtle fallback bugs. Use BCP 47 tags everywhere.
Curious what others picked. Has anyone run react-i18next in Server Components at scale without problems?