Internationalization (i18n) and localization (l10n) are often treated as one problem. They're two separate concerns: i18n is engineering (how your app handles multiple languages), l10n is content (what the translations actually say). Get the engineering right first.
next-intl Setup
npm install next-intl
// i18n.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default,
}))
// messages/en.json
{
"nav": {
"home": "Home",
"pricing": "Pricing",
"login": "Log in"
},
"pricing": {
"title": "Simple pricing",
"monthly": "{price}/month",
"annual": "{price}/year",
"cta": "Get started"
}
}
Route Structure
app/
[locale]/
layout.tsx
page.tsx
pricing/
page.tsx
layout.tsx
middleware.ts
// middleware.ts
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['en', 'es', 'fr', 'de', 'ja'],
defaultLocale: 'en',
localeDetection: true, // detect from Accept-Language header
})
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
}
Using Translations
// Server Component
import { useTranslations } from 'next-intl'
export default function PricingPage() {
const t = useTranslations('pricing')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('monthly', { price: '$29' })}</p>
<button>{t('cta')}</button>
</div>
)
}
// Client Component
'use client'
import { useTranslations } from 'next-intl'
export function NavBar() {
const t = useTranslations('nav')
return <nav><a href='/'>{t('home')}</a></nav>
}
Number and Date Formatting
import { useFormatter } from 'next-intl'
function PriceDisplay({ amount, currency }: { amount: number; currency: string }) {
const format = useFormatter()
return (
<span>
{format.number(amount, { style: 'currency', currency })}
</span>
)
}
// en: $29.00
// de: 29,00 $
// ja: $29.00
function RelativeTime({ date }: { date: Date }) {
const format = useFormatter()
return <span>{format.relativeTime(date)}</span>
// en: '3 days ago'
// es: 'hace 3 dÃas'
}
SEO with Alternate Links
// app/[locale]/layout.tsx
import { locales } from '@/i18n'
export function generateMetadata({ params: { locale } }) {
return {
alternates: {
languages: Object.fromEntries(
locales.map(l => [l, `/${l}`])
),
},
}
}
Managing Translations at Scale
For apps with multiple languages, use a translation management system:
- Crowdin: Best for open-source and community translation
- Lokalise: Good CI/CD integration
- Phrase: Enterprise-grade
Or use AI translation for a fast first pass:
// Generate translation using Claude
async function translateMessages(enMessages: object, targetLocale: string) {
const response = await anthropic.messages.create({
model: 'claude-haiku-4-5-20251001',
system: `Translate this JSON to ${targetLocale}. Keep keys unchanged. Preserve {variable} placeholders.`,
messages: [{ role: 'user', content: JSON.stringify(enMessages, null, 2) }],
})
return JSON.parse(response.content[0].text)
}
The AI SaaS Starter at whoffagents.com ships with next-intl configured for 5 locales, message files, middleware, and formatter utilities ready to use. $99 one-time.
Top comments (0)