Next.js Internationalization: Complete i18n Guide with next-intl
Over 75% of internet users prefer browsing in their native language, and businesses that localize see 70% higher conversion rates in non-English markets. Next.js combined with next-intl provides a robust internationalization framework handling routing, translations, formatting, and SEO across any number of locales.
Key Takeaways
- next-intl integrates seamlessly with Next.js App Router and server components
- Locale-prefixed routing keeps English URLs clean while adding prefixes for other languages
- Server components load translations without client-side JavaScript bundle overhead
- Proper hreflang tags and multilingual sitemaps are essential for international SEO
Project Setup
Install next-intl and organize your project with an i18n directory containing routing.ts, navigation.ts, and request.ts. Place your app under [locale] in the App Router. Store translation JSON files in a messages directory at the project root.
Routing Configuration
Define routing with defineRouting() specifying your locales array, default locale, and locale prefix strategy. The "as-needed" strategy omits the prefix for the default locale (English) while adding /es/, /fr/, /ar/ prefixes for others.
Create navigation wrappers with createNavigation(routing) to get locale-aware Link, useRouter, usePathname, and redirect functions. Use these instead of Next.js native navigation throughout your app.
Middleware Setup
In Next.js 16, create a proxy.ts file (replacing the older middleware.ts pattern). Export a proxy function created by createMiddleware(routing). Configure the matcher to exclude API routes, static files, and _next paths.
Translation Files
JSON Structure (Nested, Not Flat)
Use nested keys organized by namespace. Never use flat dot-separated keys like "home.title" -- they break next-intl namespace resolution. Structure translations as:
- common: Shared terms (buttons, labels, loading states)
- nav: Navigation items
- home: Home page content
- about: About page content
- admin.common: Shared admin terms
- admin.products: Product management page
Server Components
Use getTranslations("namespace") in async server components. This loads translations on the server with zero client bundle impact. Call t("key") for string translations, t.rich("key", { bold: (chunks) => ... }) for rich text.
Client Components
Use useTranslations("namespace") hook in client components marked with "use client". The hook provides the same t() function. Only the translations for the active namespace are included in the client bundle.
RTL (Right-to-Left) Support
For Arabic, Hebrew, Urdu, and other RTL languages, set dir="rtl" on the HTML element based on the current locale. In the locale layout, check if the locale is in your rtlLocales array and set the direction accordingly.
Use Tailwind CSS logical properties (ps, pe, ms, me instead of pl, pr, ml, mr) for layouts that automatically flip in RTL mode. Load appropriate fonts for RTL scripts (e.g., Noto Sans Arabic for Arabic and Urdu).
SEO: Metadata and Hreflang
Dynamic Metadata
Every page must use generateMetadata() (never static export const metadata) for locale-aware titles, descriptions, and hreflang alternates. Build an alternates.languages object mapping each locale to its URL path, plus an x-default entry pointing to the English version.
Multilingual Sitemap
Generate sitemap entries for every page in every locale. Each entry should include alternates.languages mapping all locale variants. This enables search engines to serve the correct language version in each market.
Content-Language Meta Tag
Set the content-language meta tag in your locale layout to help search engines and AI crawlers identify the page language. This is especially important for Bing and emerging AI search engines.
Translation Workflow
- Edit en.json as the single source of truth for all translatable strings
- Run translation script to propagate new keys to all locale files (using Google Translate API or DeepL for initial drafts)
- Review translations with native speakers for quality and cultural appropriateness
- Test RTL languages thoroughly -- check layout flipping, text alignment, and form inputs
- Verify SEO with hreflang validator tools and Google Search Console international targeting
Handling Large Translation Files
For applications with 5,000+ translation keys, organize JSON files by namespace to improve maintainability. next-intl loads only the namespaces needed for the current page, keeping performance optimal.
Performance Considerations
- Server rendering: Translations load on the server. No translation JSON is shipped to the client unless the component is a client component.
- Namespace splitting: Only active namespaces load per page, not the entire translation file.
- Static generation: Pages using getTranslations() can be statically generated at build time for all locales.
- Bundle size: Client components include only their namespace translations. A page using useTranslations("home") ships only the "home" namespace.
Common Pitfalls
- Flat keys: Using "home.title" as a key instead of nested
{ "home": { "title": "..." } }breaks namespace resolution - Missing translations: Always provide fallback behavior -- next-intl shows the key name if a translation is missing, but this looks broken to users
- Hardcoded strings: Audit your codebase for any strings outside the translation system
- Date/number formatting: Use next-intl formatters instead of toLocaleDateString() for consistent behavior
- Static metadata: Using export const metadata instead of generateMetadata() prevents locale-aware titles and descriptions
Frequently Asked Questions
Q: How many locales can next-intl handle?
There is no practical limit. Applications with 11+ locales and 7,000+ translation keys run without performance issues. Server-side rendering means translations do not bloat client bundles.
Q: Does i18n affect page load performance?
With server components, translation loading happens on the server. The performance impact is negligible. Client components include only their namespace translations, typically a few KB.
Q: How do we handle dynamic database content in multiple languages?
Static UI text uses JSON files. Database content (blog posts, product descriptions) requires storing translations in dedicated columns or tables, or using a CMS with built-in translation workflow. Some teams use a hybrid: JSON for UI, database for content.
Q: What about number and date formatting?
next-intl provides useFormatter() for locale-aware formatting of numbers, currencies, dates, and relative times. All formatting follows the Intl standard and respects locale conventions automatically.
What Is Next
Internationalization is an investment that pays dividends in every non-English market. Start with your highest-value locales and expand from there.
Contact ECOSIRE for i18n implementation help, or explore our Odoo implementation services for multilingual ERP deployment.
Published by ECOSIRE -- helping businesses scale with enterprise software solutions.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Grow Your Business with ECOSIRE
Enterprise solutions across ERP, eCommerce, AI, analytics, and automation.
Related Articles
Internationalization in Next.js: 11-Locale Implementation
Build a production-ready 11-locale Next.js app with next-intl v4. Covers routing, RTL support, translation workflows, hreflang SEO, and server/client component patterns.
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
React Query (TanStack): Data Fetching Patterns
Master TanStack Query v5 data fetching patterns: queries, mutations, optimistic updates, infinite scroll, prefetching, cache invalidation, and integration with Next.js App Router.