Internationalization
Multi-language support using next-intl — when to enable it, what's auto-translated, what isn't, and the SEO traps to avoid.
Why this matters
Internationalization is the rare directory feature that's free to ship in code but expensive to maintain — every translated string is a long-term commitment. Most directories should ship in one language, validate the niche, and only add locales after demand is proven. But when you do enable i18n, the wiring matters: locale-prefixed URLs change your sitemap, hreflang tags affect indexing, and middleware routing has to play nicely with auth refreshes.
This page covers the toggles, the components, and the SEO implications. Read Common pitfalls before flipping i18n: true — most of the issues we see are subtle and only show up after content is live.
Off by default. When on, the site supports multiple languages with URL prefixes like /en/, /ru/, /es/.
Turning i18n on with only en.json gives you every downside (extra routing, prefixed URLs) with zero benefit. Only flip it on when you have at least one real translation ready.
How it works
- Translations live in
messages/{locale}.json - Middleware (
middleware.ts) detects the user's language and routes accordingly - Components use the
useTranslations()hook to pull strings - A language switcher in the header lets users override detection
Setup
Two toggles, both required:
export const featuresConfig = { /* ... */ i18n: true };export const i18nConfig = {
enabled: true,
defaultLocale: 'en',
locales: ['en', 'ru', 'es'],
localeCookie: 'NEXT_LOCALE',
localeDetection: true,
};Then create translation files:
messages/
├── en.json
├── ru.json
└── es.json
Using translations in components
Server components
import { getTranslations } from 'next-intl/server';
export default async function Page() {
const t = await getTranslations('homepage');
return <h1>{t('title')}</h1>;
}Client components
'use client';
import { useTranslations } from 'next-intl';
export function Hero() {
const t = useTranslations('homepage');
return <h1>{t('title')}</h1>;
}Navigating
Always use next-intl's Link, not Next's — it auto-prefixes with the current locale:
import { Link } from '@/i18n/navigation'; // or next-intl equivalent
<Link href="/pricing">Pricing</Link>
// renders as /pricing for EN, /ru/pricing for RU, etc.What's not auto-translated
- User-generated content (project names, descriptions, comments) — stays in whatever language the user typed
- Blog posts — write separate MDX files per locale, or use a translation service
- Legal pages — translate the strings in
messages/, the content lives there
A real-world walkthrough: adding Spanish to a live English directory
You've shipped your directory in English, validated the niche, and want to add Spanish. The 30-minute path:
- Translate
messages/en.jsontomessages/es.json(DeepL handles ~85% of strings well; review the rest manually for tone). - Set
i18nConfig.locales = ['en', 'es']andi18nConfig.enabled = true. KeepdefaultLocale: 'en'so your existing English URLs (/,/categories,/project/foo) continue to resolve. - Build and deploy. Your sitemap will now contain
/es/-prefixed URLs alongside the unchanged English ones. Existing English URLs and SEO signals stay intact. - Add hreflang annotations in your root layout via
next-intl's helpers — this tells Google "the Spanish version of/foolives at/es/foo" so it doesn't dilute either page's authority. - Don't auto-translate user-generated content. Project titles, descriptions and comments stay in whatever language the submitter wrote them. The directory shell translates; the content does not.
If you instead use localePrefix: 'always', all URLs (English included) gain a /en/ prefix — that's a one-time SEO migration with 301 redirects required. Plan accordingly.
Common pitfalls
- Enabling i18n with only one translation file. You get all the routing complexity (URL prefixes, middleware overhead, hreflang management) and zero benefit. Wait until you have a complete translation ready.
- Forgetting to handle locale in dynamic routes. A
/project/[slug]page that hardcodes English copy in the React component will render English even on/es/project/foo. Run a final pass: every component using user-facing strings must useuseTranslations()orgetTranslations(). - Letting middleware step on the OAuth callback. With i18n on, middleware tries to detect locale on every request — including
/auth/callback. Add explicit middleware skips for auth callback and webhook routes or you'll see flaky sign-in flows. - Translating slugs. Don't.
/categories/saasand/es/categories/saasshould share the same slug; only the rendered page text should differ. Translating slugs creates a SEO-unfriendly two-tree URL graph and breaks every existing link. - Mixing
Linkfrom Next.js withLinkfrom next-intl. The Next.jsLinkskips locale prefixing and you'll randomly drop users back to the default locale. Use the next-intlLinkeverywhere — set up an ESLint rule if you want to enforce it.
FAQ
Should I serve translated blog posts from a single MDX file or one per locale?
One per locale. Blog posts are long-form content and machine translation rarely produces SEO-acceptable output. Create content/blog/foo.mdx and content/blog/foo.es.mdx, then route each from a locale-aware loader. The index pages can show translated titles via the same messages/ system if needed.
How do I detect a user's preferred language?
Middleware does it automatically — it reads the Accept-Language header on the first request, sets a NEXT_LOCALE cookie, and uses that for subsequent requests. Users override via the language switcher. If you need explicit control, set localeDetection: false in config and route everyone to your default locale.
Will i18n hurt my SEO?
If done correctly, no — Google handles locale-prefixed sites natively when hreflang is set. If done incorrectly (translated slugs, missing hreflang, leaking English content into translated pages), it can split authority across duplicate URLs and slow indexing. Test with site:yourdomain.com inurl:/es/ after launch to verify Google picked up the translated tree.
See also
- i18n Config
- next-intl docs
- Features Config — toggling i18n off/on
- SEO for small directories — how locale URLs interact with indexing