Internationalization in Next.js App Router: The Complete Implementation Without Third-Party Libraries
Internationalization (i18n) in Next.js App Router does not need next-intl, react-i18next, or any heavy library. The App Router's architecture (server components, middleware, dynamic routes) provides the primitives you need. A dictionary pattern with TypeScript gives you type-safe translations with zero client-side JavaScript overhead for static text.
Here is the complete implementation we considered for AIDToolStack (and how you can add multi-language support to any App Router project).
The Architecture
The approach: URL-based locale detection (/en/tools, /fr/tools, /de/tools), middleware that detects and redirects to the user's preferred locale, server-side dictionary loading (zero bundle impact for translations), and type-safe translation keys (TypeScript catches missing translations at build time).
No library needed. The entire system is: middleware (locale detection/redirect), a dictionary loader (JSON files per locale), a server component helper (access translations in RSC), and a client component hook (access translations in interactive components).
Step 1: Locale-Based Routing
Structure your app directory with a [locale] dynamic segment: app/[locale]/page.tsx, app/[locale]/tools/page.tsx, etc.
Every page receives the locale as a parameter. The URL determines the language: /en/tools shows English, /fr/tools shows French.
Benefits: SEO-friendly (each locale has its own URL — search engines index each language separately), shareable (users can share a link in their language), and cacheable (different URLs = different cache entries).
Step 2: Middleware for Locale Detection
Middleware runs on every request and: checks if the URL already has a locale prefix, if not: detects the user's preferred language from the Accept-Language header, redirects to the URL with their preferred locale prefix, and sets a cookie to remember their choice.
The detection priority: explicit URL (user navigated to /fr/...), cookie (previously selected language), Accept-Language header (browser preference), and default locale (English as fallback).
This means: first-time visitors are automatically redirected to their language. Returning visitors get their previously chosen language. And direct links always work regardless of preference.
Step 3: Translation Dictionaries
Create JSON files per locale: dictionaries/en.json, dictionaries/fr.json, dictionaries/de.json.
Structure translations by page or feature: { "tools": { "title": "Developer Tools", "search_placeholder": "Search tools..." }, "common": { "loading": "Loading...", "error": "Something went wrong" } }.
Load the appropriate dictionary server-side based on the locale parameter. In server components: import the dictionary at the top of the page component. The JSON is resolved at build/render time — zero client JavaScript for translations.
Step 4: Type-Safe Translation Keys
Define a TypeScript type from your English dictionary (the source of truth). All other locale dictionaries must match this type. If a translation key is missing in French: TypeScript throws a compile error.
This prevents: shipping a page with missing translations (caught at build time), typos in translation keys (autocomplete from the type), and drift between locales (all must implement the same keys).
The type is derived from the English JSON structure using TypeScript's recursive type utilities. Any nested key path is a valid translation access.
Step 5: Server Component Usage
In server components (the majority of your pages): load the dictionary based on the locale param and access translations directly. No hooks, no context, no client-side overhead.
The dictionary is just a JavaScript object. Access translations with dot notation: dict.tools.title. TypeScript validates every access.
For server components: this is the entire implementation. Load dictionary, use values, done. Zero client-side JavaScript for translations.
Step 6: Client Component Usage
Interactive components ("use client") cannot directly access server-side data. Pattern: pass translated strings as props from a server component parent, or use a client-side context that receives the dictionary from the server.
We prefer props: the server component loads the dictionary and passes relevant strings to client components. This minimizes the translation data sent to the client (only strings that interactive components need, not the entire dictionary).
For components that need many translations: a lightweight context provider that receives the dictionary from the server layout and makes it available to all client components within that layout.
Step 7: Dynamic Content Translation
Static UI strings (buttons, labels, headings) are handled by dictionaries. Dynamic content (user-generated content, database content) needs a different approach.
Options: store content in multiple languages in the database (each blog post has title_en, title_fr, or a translations JSON field), use AI translation for user content (with human review for important content), or serve dynamic content in the original language with a "translate" button.
For our tool directory: tool names and descriptions are user-submitted in English. We do not auto-translate these (tool names should not be translated — "Prisma" stays "Prisma"). UI chrome (navigation, buttons, labels) is translated via dictionaries.
Step 8: SEO for Multi-Language
Each locale has its own URL and its own metadata. The generateMetadata function receives the locale parameter and returns locale-specific titles and descriptions.
Add hreflang tags: tell search engines about the relationship between language versions. Your /en/tools page has hreflang links to /fr/tools and /de/tools. This prevents search engines from treating them as duplicate content.
Add a sitemap per locale: each language version appears in the sitemap with its locale-specific URL.
Step 9: Language Switcher
The language switcher component: shows available languages, links to the current page in the selected language (/en/tools → /fr/tools), and preserves query parameters and path.
Implementation: use the current pathname, replace the locale segment, and link to the new URL. Set the locale cookie so the choice persists.
Step 10: Right-to-Left (RTL) Support
If you support RTL languages (Arabic, Hebrew): add dir="rtl" to the html element based on locale. Use CSS logical properties (margin-inline-start instead of margin-left) for layout that automatically adapts to text direction.
Tailwind CSS supports RTL with the rtl: variant: rtl:mr-4 applies right margin only in RTL mode.
Performance Considerations
Server Components + dictionaries: zero client-side JavaScript for static translations. This is the primary benefit over library-based approaches (react-i18next loads the entire translation library and dictionary on the client).
Dictionary splitting: do not load the entire dictionary for every page. Split by route: tools pages load tools translations, blog pages load blog translations. Each page only loads what it needs.
Build-time optimization: for static pages (generated at build time): translations are baked into the HTML. No runtime dictionary loading at all.
When to Use a Library Instead
Use next-intl or similar when: you need ICU message format (plurals, gender, numbers with locale-specific formatting), your translation workflow involves professional translators using tools like Crowdin or Lokalise (they integrate with library formats), or you have complex interpolation needs (variables, HTML within translations, conditional logic).
For simple applications (static text in multiple languages): the dictionary pattern is lighter, faster, and requires no dependencies.
Our Decision
AIDToolStack currently serves English only. When we add languages: we will use this dictionary pattern. Our content is mostly tool listings and UI chrome — perfect for simple key-value translations. The server component architecture means zero translation overhead on the client. And TypeScript ensures we never ship a page with missing translations.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.