Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
By integrating the Intlayer MCP Server to your favourite AI assistant can retrieve all the doc directly from ChatGPT, DeepSeek, Cursor, VSCode, etc.
See MCP Server docThe content of this page was translated using an AI.
See the last version of the original content in EnglishIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
next-i18next VS next-intl VS intlayer | Next.js Internationalisation (i18n)
Let’s take a look into the similarities and differences between three i18n options for Next.js: next-i18next, next-intl, and Intlayer.
This is not a full tutorial. It’s a comparison to help you pick.
We focus on Next.js 13+ App Router (with React Server Components) and evaluate:
- Architecture & content organisation
- TypeScript & safety
- Missing translation handling
- Routing & middleware
- Performance & loading behaviour
- Developer experience (DX), tooling & maintenance
- SEO & large-project scalability
tl;dr: All three can localise a Next.js app. If you want component-scoped content, strict TypeScript types, build-time missing-key checks, tree-shaken dictionaries, and first-class App Router + SEO helpers, Intlayer is the most complete, modern choice.
One confusion often made by developers is to think that next-intl is the Next.js version of react-intl. It's not—next-intl is maintained by Amann, while react-intl is maintained by FormatJS.
In short
- next-intl - Lightweight, straightforward message formatting with solid Next.js support. Centralised catalogues are common; DX is simple, but safety and large-scale maintenance remain mostly your responsibility.
- next-i18next - i18next in Next.js clothing. Mature ecosystem and features via plugins (e.g., ICU), but configuration can be verbose and catalogues tend to centralise as projects grow.
- Intlayer - Component-centric content model for Next.js, strict TS typing, build-time checks, tree-shaking, built-in middleware & SEO helpers, optional Visual Editor/CMS, and AI-assisted translations.
Badges update automatically. Snapshots will vary over time.
Side-by-Side Feature Comparison (Next.js focused)
Feature | next-intlayer (Intlayer) | next-intl | next-i18next |
---|---|---|---|
Translations Near Components | ✅ Yes, content collocated with each component | ❌ No | ❌ No |
TypeScript Integration | ✅ Advanced, auto-generated strict types | ✅ Good | ⚠️ Basic |
Missing Translation Detection | ✅ TypeScript error highlight and build-time error/warning | ⚠️ Runtime fallback | ⚠️ Runtime fallback |
Rich Content (JSX/Markdown/components) | ✅ Direct support | ❌ Not designed for rich nodes | ⚠️ Limited |
AI-powered Translation | ✅ Yes, supports multiple AI providers. Usable using your own API keys. Considers the context of your application and content scope | ❌ No | ❌ No |
Visual Editor | ✅ Yes, local Visual Editor + optional CMS; can externalise codebase content; embeddable | ❌ No / available via external localisation platforms | ❌ No / available via external localisation platforms |
Localised Routing | ✅ Yes, supports localised paths out of the box (works with Next.js & Vite) | ✅ Built-in, App Router supports [locale] segment | ✅ Built-in |
Dynamic Route Generation | ✅ Yes | ✅ Yes | ✅ Yes |
Pluralisation | ✅ Enumeration-based patterns | ✅ Good | ✅ Good |
Formatting (dates, numbers, currencies) | ✅ Optimised formatters (Intl under the hood) | ✅ Good (Intl helpers) | ✅ Good (Intl helpers) |
Content Format | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml WIP) | ✅ .json, .js, .ts | ⚠️ .json |
ICU support | ⚠️ WIP | ✅ Yes | ⚠️ Via plugin (i18next-icu) |
SEO Helpers (hreflang, sitemap) | ✅ Built-in tools: helpers for sitemap, robots.txt, metadata | ✅ Good | ✅ Good |
Ecosystem / Community | ⚠️ Smaller but growing fast and reactive | ✅ Good | ✅ Good |
Server-side Rendering & Server Components | ✅ Yes, streamlined for SSR / React Server Components | ⚠️ Supported at page level but need to pass t-functions on component tree for children server components | ⚠️ Supported at page level but need to pass t-functions on component tree for children server components |
Tree-shaking (load only used content) | ✅ Yes, per-component at build time via Babel/SWC plugins | ⚠️ Partial | ⚠️ Partial |
Lazy loading | ✅ Yes, per-locale / per-dictionary | ✅ Yes (per-route/per-locale), requires namespace management | ✅ Yes (per-route/per-locale), requires namespace management |
Purge unused content | ✅ Yes, per-dictionary at build time | ❌ No, can be managed manually with namespace management | ❌ No, can be managed manually with namespace management |
Management of Large Projects | ✅ Encourages modular, suited for design system | ✅ Modular with setup | ✅ Modular with setup |
Testing Missing Translations (CLI/CI) | ✅ CLI: npx intlayer content test (CI-friendly audit) | ⚠️ Not built-in; docs suggest npx @lingual/i18n-check | ⚠️ Not built-in; rely on i18next tools / runtime saveMissing |
Introduction
Next.js provides built-in support for internationalised routing (e.g. locale segments). However, that feature does not perform translations on its own. You still need a library to render localised content to your users.
Many i18n libraries exist, but in the Next.js ecosystem today, three are gaining traction: next-i18next, next-intl, and Intlayer.
Architecture & scalability
- next-intl / next-i18next: Default to centralised catalogues per locale (plus namespaces in i18next). Works fine early on, but often becomes a large shared surface area with increasing coupling and key churn.
- Intlayer: Encourages per-component (or per-feature) dictionaries co-located with the code they serve. This reduces cognitive load, facilitates duplication/migration of UI pieces, and minimises cross-team conflicts. Unused content is naturally easier to identify and remove.
Why it matters: In large codebases or design-system setups, modular content scales better than monolithic catalogues.
Bundle sizes & dependencies
After building the application, the bundle is the JavaScript that the browser will load to render the page. Bundle size is therefore important for application performance.
Two components are important in the context of a multi-language application bundle:
- The application code
- The content loaded by the browser
Application Code
The significance of application code is minimal in this case. All three solutions are tree-shakable, meaning that unused parts of the code are not included in the bundle.
Here is a comparison of the JavaScript bundle size loaded by the browser for a multi-language application with the three solutions.
If we do not require any formatter in the application, the list of exported functions after tree-shaking will be:
- next-intlayer: useIntlayer, useLocale, NextIntlClientProvider, (Bundle size is 180.6 kB -> 78.6 kB (gzip))
- next-intl: useTranslations, useLocale, NextIntlClientProvider, (Bundle size is 101.3 kB -> 31.4 kB (gzip))
- next-i18next: useTranslation, useI18n, I18nextProvider, (Bundle size is 80.7 kB -> 25.5 kB (gzip))
These functions are merely wrappers around React context/state, so the overall impact of the i18n library on bundle size is minimal.
Intlayer is slightly larger than next-intl and next-i18next because it incorporates more logic in the useIntlayer function. This is related to markdown and intlayer-editor integration.
Content and Translations
This part is often ignored by developers, but let us consider the case of an application composed of 10 pages in 10 languages. Let us assume that each page integrates 100% unique content to simplify the calculation (in reality, much content is redundant between pages, e.g., page title, header, footer, etc.).
A user wanting to visit the /fr/about page will load the content of one page in a given language. Ignoring content optimisation would mean loading 8,200% ((1 + (((10 pages - 1) × (10 languages - 1)))) × 100) of the application content unnecessarily. Do you see the problem? Even if this content remains text, and while you probably prefer to think about optimising your site's images, you are sending useless content across the globe and making users' computers process it for nothing.
Two important issues:
Splitting by route:
If I'm on the /about page, I don't want to load the content of the /home page
Splitting by locale:
If I'm on the /fr/about page, I don't want to load the content of the /en/about page
Again, all three solutions are aware of these issues and allow managing these optimisations. The difference between the three solutions is the DX (Developer Experience).
next-intl and next-i18next use a centralised approach to manage translations, allowing splitting JSON by locale and by sub-files. In next-i18next, we call the JSON files 'namespaces'; next-intl allows declaring messages. In intlayer, we call the JSON files 'dictionaries'.
- In the case of next-intl, like next-i18next, content is loaded at the page/layout level, then this content is loaded into a context provider. This means the developer must manually manage the JSON files that will be loaded for each page.
In practice, this implies that developers often skip this optimisation, preferring to load all content in the page's context provider for simplicity.
- In the case of intlayer, all content is loaded in the application. Then a plugin (@intlayer/babel / @intlayer/swc) takes care of optimising the bundle by loading only the content used on the page. The developer therefore doesn't need to manually manage the dictionaries that will be loaded. This allows better optimisation, better maintainability, and reduces development time.
As the application grows (especially when multiple developers work on the application), it is common to forget to remove content that is no longer used from JSON files.
Note that all JSON is loaded in all cases (next-intl, next-i18next, intlayer).
This is why Intlayer's approach is more performant: if a component is no longer used, its dictionary is not loaded in the bundle.
How the library handles fallbacks is also important. Let us consider that the application is in English by default, and the user visits the /fr/about page. If translations are missing in French, we will consider the English fallback.
In the case of next-intl and next-i18next, the library requires loading the JSON related to the current locale, but also to the fallback locale. Thus, considering that all content has been translated, each page will load 100% unnecessary content. In comparison, intlayer processes the fallback at dictionary build time. Thus, each page will load only the content used.
Here is an example of the impact of bundle size optimisation using intlayer in a vite + react application:
Optimised bundle | Bundle not optimised |
---|---|
![]() | ![]() |
TypeScript & safety
next-intl
- Solid TypeScript support, but keys aren’t strictly typed by default; you’ll maintain safety patterns manually.
next-i18next
- Base typings for hooks; strict key typing requires extra tooling/config.
intlayer
- Generates strict types from your content. IDE autocompletion and compile-time errors catch typos and missing keys before deployment.
Why it matters: Strong typing shifts failures left (CI/build) instead of right (runtime).
Missing translation handling
next-intl
- Relies on runtime fallbacks (e.g., show the key or default locale). Build doesn’t fail.
next-i18next
- Relies on runtime fallbacks (e.g., show the key or default locale). Build doesn’t fail.
intlayer
- Build-time detection with warnings/errors for missing locales or keys.
Why it matters: Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
Routing, middleware & URL strategy
Generates strict types from your content. IDE autocompletion and compile-time errors catch typos and missing keys before deployment.
</Column> </Columns>
Why it matters: Strong typing shifts failures left (CI/build) instead of right (runtime).
Missing translation handling
next-intl
- Relies on runtime fallbacks (e.g., show the key or default locale). Build does not fail.
next-i18next
- Relies on runtime fallbacks (e.g., show the key or default locale). Build does not fail.
intlayer
- Build-time detection with warnings/errors for missing locales or keys.
Why it matters: Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
Routing, middleware & URL strategy
next-intl
- Works with Next.js localised routing on the App Router.
next-i18next
- Works with Next.js localised routing on the App Router.
intlayer
- All of the above, plus i18n middleware (locale detection via headers/cookies) and helpers to generate localised URLs and <link rel="alternate" hreflang="…"> tags.
Why it matters: Fewer custom glue layers; consistent UX and clean SEO across locales.
Server Components (RSC) alignment
next-intl
- Supports Next.js 13+. Often requires passing t-functions/formatters through component trees in hybrid setups.
next-i18next
- Supports Next.js 13+. Similar constraints with passing translation utilities across boundaries.
intlayer
- Supports Next.js 13+ and smooths the server/client boundary with a consistent API and RSC-oriented providers, avoiding shuttling formatters or t-functions.
Why it matters: Cleaner mental model and fewer edge cases in hybrid trees.
DX, tooling & maintenance
next-intl
- Commonly paired with external localisation platforms and editorial workflows.
next-i18next
- Commonly paired with external localisation platforms and editorial workflows.
intlayer
- Ships a free Visual Editor and optional CMS (Git-friendly or externalised), plus a VSCode extension and AI-assisted translations using your own provider keys.
Why it matters: Lowers operational costs and shortens the feedback loop between developers and content authors.
Integration with localisation platforms (TMS)
Large organisations often rely on Translation Management Systems (TMS) like Crowdin, Phrase, Lokalise, Localizely, or Localazy.
Why companies care
- Collaboration & roles: Multiple actors are involved: developers, product managers, translators, reviewers, marketing teams.
- Scale & efficiency: continuous localisation, in‑context review.
next-intl / next-i18next
- Typically use centralised JSON catalogues, so export/import with TMS is straightforward.
- Mature ecosystems and examples/integrations for the platforms above.
Intlayer
- Encourages decentralised, per-component dictionaries and supports TypeScript/TSX/JS/JSON/MD content.
- This improves modularity in code, but can make plug‑and‑play TMS integration harder when a tool expects centralised, flat JSON files.
- Intlayer provides alternatives: AI‑assisted translations (using your own provider keys), a Visual Editor/CMS, and CLI/CI workflows to catch and prefill gaps.
Note: next-intl and i18next also accept TypeScript catalogues. If your team stores messages in .ts files or decentralises them by feature, you may encounter similar TMS friction. However, many next-intl setups remain centralised in a locales/ folder, which is somewhat easier to refactor to JSON for TMS.
Developer Experience
This section provides a detailed comparison between the three solutions. Rather than considering simple cases, as described in the 'getting started' documentation for each solution, we will examine a real use case, more akin to an actual project.
App structure
The app structure is important to ensure good maintainability of your codebase.
.├── locales│ ├── en│ │ ├── home.json│ │ └── navbar.json│ ├── fr│ │ ├── home.json│ │ └── navbar.json│ └── es│ ├── home.json│ └── navbar.json├── i18n.ts└── src ├── middleware.ts ├── app │ └── home.tsx └── components └── Navbar └── index.tsx
Comparison
- next-intl / next-i18next: Centralised catalogues (JSON; namespaces/messages). Clear structure, integrates well with translation platforms, but can lead to more cross-file edits as apps grow.
- Intlayer: Per-component .content.{ts|js|json} dictionaries co-located with components. Easier component reuse and local reasoning; adds files and relies on build-time tooling.
Setup and Loading Content
As mentioned previously, you must optimise how each JSON file is imported into your code. How the library handles content loading is important.
Copy the code to the clipboard
import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";// Can be imported from a shared configconst locales = ["en", "fr", "es"];export default getRequestConfig(async ({ locale }) => { // Validate that the incoming `locale` parameter is valid if (!locales.includes(locale as any)) notFound(); return { messages: (await import(`../messages/${locale}.json`)).default, };});
Copy the code to the clipboard
import { NextIntlClientProvider } from "next-intl";import { getMessages, unstable_setRequestLocale } from "next-intl/server";import pick from "lodash/pick";export default async function LocaleLayout({ children, params,}: { children: React.ReactNode; params: { locale: string };}) { const { locale } = params; // Set the active request locale for this server render (RSC) unstable_setRequestLocale(locale); // Messages are loaded server-side via src/i18n/request.ts // (see next-intl docs). Here we only send a subset to the client // that is needed for client components (payload optimisation). const messages = await getMessages(); const clientMessages = pick(messages, ["common", "about"]); return ( <html lang={locale}> <body> <NextIntlClientProvider locale={locale} messages={clientMessages}> {children} </NextIntlClientProvider> </body> </html> );}
Copy the code to the clipboard
import { getTranslations } from "next-intl/server";import { ClientComponent, ServerComponent } from "@components";export default async function LandingPage({ params,}: { params: { locale: string };}) { // Strictly server-side loading (not hydrated on the client) const t = await getTranslations("about"); return ( <main> <h1>{t("title")}</h1> <ClientComponent /> <ServerComponent /> </main> );}
Comparison
All three support per-locale content loading and providers.
With next-intl/next-i18next, you typically load selected messages/namespaces per route and place providers where needed.
With Intlayer, it adds build-time analysis to infer usage, which can reduce manual wiring and may allow a single root provider.
Choose between explicit control and automation based on team preference.
Usage in a client component
Let's take an example of a client component rendering a counter.
Translations (shape reused; load them into next-intl messages as you prefer)
Copy the code to the clipboard
{ "counter": { "label": "Counter", "increment": "Increment" }}
Copy the code to the clipboard
{ "counter": { "label": "Compteur", "increment": "Incrémenter" }}
Client component
Copy the code to the clipboard
"use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => { // Scope directly to the nested object const t = useTranslations("about.counter"); const format = useFormatter(); const [count, setCount] = useState(0); return ( <div> <p>{format.number(count)}</p> <button aria-label={t("label")} onClick={() => setCount((count) => count + 1)} > {t("increment")} </button> </div> );};
Don't forget to add the "about" message to the page client message
Comparison
Number formatting
- next-i18next: no useNumber; use Intl.NumberFormat (or i18next-icu).
- next-intl: useFormatter().number(value).
- Intlayer: built-in useNumber().
Keys
- Maintain a nested structure (about.counter.label) and scope your hook accordingly (useTranslation("about") + t("counter.label") or useTranslations("about.counter") + t("label")).
File locations
- next-i18next expects JSON in public/locales/{lng}/{ns}.json.
- next-intl is flexible; load messages however you configure.
- Intlayer stores content in TS/JS dictionaries and resolves by key.
Usage in a server component
We will take the case of a UI component. This component is a server component, and should be able to be inserted as a child of a client component. (page (server component) -> client component -> server component). As this component can be inserted as a child of a client component, it cannot be async.
Copy the code to the clipboard
type ServerComponentProps = { count: number; t: (key: string) => string;};const ServerComponent = ({ t, count }: ServerComponentProps) => { const formatted = new Intl.NumberFormat(i18n.language).format(count); return ( <div> <p>{formatted}</p> <button aria-label={t("label")}>{t("increment")}</button> </div> );};
As the server component cannot be async, you need to pass the translations and formatter function as props.
- const t = await getTranslations("about.counter");
- const format = await getFormatter();
Intlayer exposes server-safe hooks via next-intlayer/server. To function, useIntlayer and useNumber use hook-like syntax, similar to the client hooks, but rely under the hood on the server context (IntlayerServerProvider).
Metadata / Sitemap / Robots
Translating content is excellent. However, people often forget that the main goal of internationalisation is to make your website more visible to the world. I18n is an incredible lever to enhance your website's visibility.
Here is a list of best practices regarding multilingual SEO.
- set hreflang meta tags in the <head> tag > This helps search engines to understand which languages are available on the page
- list all page translations in the sitemap.xml using the http://www.w3.org/1999/xhtml XML schema >
- do not forget to exclude prefixed pages from the robots.txt (e.g. /dashboard, and /fr/dashboard, /es/dashboard) >
- use a custom Link component to redirect to the most localised page (e.g. in French <a href="/fr/about">À propos</a>) >
Developers often forget to properly reference their pages across locales.
Copy the code to the clipboard
import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, url)]) ); return { title: t("title"), description: t("description"), alternates: { canonical: localizedPath(locale, url), languages: { ...languages, "x-default": url }, }, };}// ... Rest of the page code
Copy the code to the clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export default function sitemap(): MetadataRoute.Sitemap { const aboutLanguages = Object.fromEntries( locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [ { url: formatterLocalizedPath(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages: aboutLanguages }, }, ];}
Copy the code to the clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...withAllLocales("/dashboard"), ...withAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: origin, sitemap: origin + "/sitemap.xml", };}
Intlayer provides a getMultilingualUrls function to generate multilingual URLs for your sitemap.
And the winner is…
It’s not simple. Each option has trade-offs. Here’s how I see it:
next-intl
- simplest, lightweight, fewer decisions forced on you. If you want a minimal solution, you’re comfortable with centralised catalogues, and your app is small to mid-size.
next-i18next
- mature, full of features, lots of community plugins, but higher setup cost. If you need i18next’s plugin ecosystem (e.g., advanced ICU rules via plugins) and your team already knows i18next, accepting more configuration for flexibility.
Intlayer
- built for modern Next.js, with modular content, type safety, tooling, and less boilerplate. If you value component-scoped content, strict TypeScript, build-time guarantees, tree-shaking, and batteries-included routing/SEO/editor tooling – especially for Next.js App Router, design systems and large, modular codebases.
If you prefer minimal setup and accept some manual wiring, next-intl is a good choice. If you need all the features and don’t mind complexity, next-i18next works. But if you want a modern, scalable, modular solution with built-in tools, Intlayer aims to provide that out of the box.
Alternative for enterprise teams: If you need a well-proven solution that works perfectly with established localisation platforms like Crowdin, Phrase, or other professional translation management systems, consider next-intl or next-i18next for their mature ecosystem and proven integrations.
Future roadmap: Intlayer also plans to develop plugins that work on top of i18next and next-intl solutions. This will give you the advantages of Intlayer for automation, syntax, and content management while keeping the security and stability provided by these established solutions in your application code.
GitHub STARs
GitHub stars are a strong indicator of a project's popularity, community trust, and long-term relevance. While not a direct measure of technical quality, they reflect how many developers find the project useful, follow its progress, and are likely to adopt it. For estimating the value of a project, stars help compare traction across alternatives and provide insights into ecosystem growth.
Conclusion
All three libraries succeed at core localisation. The difference is how much work you must do to achieve a robust, scalable setup in modern Next.js:
- With Intlayer, modular content, strict TS, build-time safety, tree-shaken bundles, and first-class App Router + SEO tooling are defaults, not chores.
- If your team prizes maintainability and speed in a multi-locale, component-driven app, Intlayer offers the most complete experience today.
Refer to 'Why Intlayer?' doc for more details.