Build applications that work seamlessly across languages and regions.
Setting Up i18n in Next.js#
1// next.config.js
2module.exports = {
3 i18n: {
4 locales: ['en', 'es', 'fr', 'de'],
5 defaultLocale: 'en',
6 },
7};Using next-intl#
1// messages/en.json
2{
3 "home": {
4 "title": "Welcome to our app",
5 "description": "Build something amazing"
6 },
7 "common": {
8 "submit": "Submit",
9 "cancel": "Cancel"
10 }
11}
12
13// messages/es.json
14{
15 "home": {
16 "title": "Bienvenido a nuestra app",
17 "description": "Construye algo increíble"
18 },
19 "common": {
20 "submit": "Enviar",
21 "cancel": "Cancelar"
22 }
23}1// app/[locale]/page.tsx
2import { useTranslations } from 'next-intl';
3
4export default function Home() {
5 const t = useTranslations('home');
6
7 return (
8 <div>
9 <h1>{t('title')}</h1>
10 <p>{t('description')}</p>
11 </div>
12 );
13}Interpolation and Plurals#
1// messages/en.json
2{
3 "greeting": "Hello, {name}!",
4 "items": {
5 "zero": "No items",
6 "one": "One item",
7 "other": "{count} items"
8 }
9}
10
11// Usage
12t('greeting', { name: 'John' }); // "Hello, John!"
13t('items', { count: 5 }); // "5 items"Formatting Numbers and Dates#
1import { useFormatter } from 'next-intl';
2
3function PriceDisplay({ amount }: { amount: number }) {
4 const format = useFormatter();
5
6 return (
7 <span>
8 {format.number(amount, { style: 'currency', currency: 'USD' })}
9 </span>
10 );
11}
12
13function DateDisplay({ date }: { date: Date }) {
14 const format = useFormatter();
15
16 return (
17 <span>
18 {format.dateTime(date, { dateStyle: 'full' })}
19 </span>
20 );
21}Language Switcher#
1'use client';
2
3import { useRouter, usePathname } from 'next/navigation';
4import { useLocale } from 'next-intl';
5
6const locales = [
7 { code: 'en', name: 'English' },
8 { code: 'es', name: 'Español' },
9 { code: 'fr', name: 'Français' },
10];
11
12export function LanguageSwitcher() {
13 const router = useRouter();
14 const pathname = usePathname();
15 const currentLocale = useLocale();
16
17 const switchLocale = (newLocale: string) => {
18 const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`);
19 router.push(newPath);
20 };
21
22 return (
23 <select
24 value={currentLocale}
25 onChange={(e) => switchLocale(e.target.value)}
26 >
27 {locales.map(locale => (
28 <option key={locale.code} value={locale.code}>
29 {locale.name}
30 </option>
31 ))}
32 </select>
33 );
34}RTL Support#
1// Handle right-to-left languages
2const rtlLocales = ['ar', 'he', 'fa'];
3
4function Layout({ locale, children }) {
5 const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr';
6
7 return (
8 <html lang={locale} dir={dir}>
9 <body>{children}</body>
10 </html>
11 );
12}1/* RTL-aware styles */
2.sidebar {
3 margin-inline-start: 1rem; /* Works for both LTR and RTL */
4}
5
6[dir="rtl"] .icon-arrow {
7 transform: scaleX(-1);
8}Server-Side Translation#
1import { getTranslations } from 'next-intl/server';
2
3export async function generateMetadata({ params: { locale } }) {
4 const t = await getTranslations({ locale, namespace: 'meta' });
5
6 return {
7 title: t('title'),
8 description: t('description'),
9 };
10}Best Practices#
- Extract all strings: Never hardcode text in components
- Use namespaces: Organize translations by feature/page
- Handle plurals: Different languages have different plural rules
- Test RTL: Verify layout works for right-to-left languages
- Lazy load: Only load translations for current locale
Keep translations close to code, use context for ambiguous terms, and always test with native speakers.