The boilerplate includes a comprehensive internationalization system using URL-based routing with BCP 47 locale codes. All text content is stored in JSON message files and accessed through a type-safe translation function.

URL-Based Routing

Locale in URL path: /fr-FR/pricing, /fr-CH/pricing, /en-US/pricing, /en-CA/pricing

JSON Messages

Organized translation files with nested keys and parameter support.

Type-Safe

TypeScript types for locales and translation keys.

Auto-Detection

Browser language detection with cookie persistence.

File Structure

i18n/
├── config.ts              # Locale configuration & utilities
├── request.ts             # next-intl setup (if used)
└── messages/
    ├── fr-FR.json         # French translations
    ├── fr-CH.json         # Swiss French translations
    ├── en-US.json         # US English translations
    └── en-CA.json         # Canadian English translations

lib/i18n/
└── index.ts               # Translation utilities

Configuration

Locale configuration lives in i18n/config.ts. This file defines the list of supported locales (e.g., fr-FR, en-US, en-CA, fr-CH), the default locale, locale display names, flag emojis, and utility functions for locale normalization and language code extraction.

Utility Functions

The i18n/config.ts module provides several helpers: getIntlLocale() converts BCP 47 codes to Intl-compatible formats, getOgLocale() converts to OpenGraph format, normalizeLocale() converts shorthand codes to full locale strings, and getLanguageCode() extracts the language portion.

Using Translations

In server components, use the async getTranslations() function with the locale from URL parameters to get a translation function. Access translations with dot notation (e.g., t('dashboard.title')). In client components, pass the needed translations as props from the parent server component since client components cannot use async functions.

Server Components (Recommended)

Resolve the translation function from the route's locale param and call it with dot-notation keys:

import { getTranslations, type Locale } from '@/lib/i18n'

export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params
  const t = await getTranslations(locale as Locale)

  return <h1>{t('dashboard.title')}</h1>
}

Client Components

Client components cannot call the async getTranslations(). Resolve the strings in the parent Server Component and pass them down as props:

// Server Component
<SubmitButton translations={{ submit: t('common.submit') }} />

// Client Component ('use client')
function SubmitButton({ translations }: { translations: { submit: string } }) {
  return <button>{translations.submit}</button>
}

Translation File Structure

Translations are stored as nested JSON files in i18n/messages/, one per locale. Keys are organized by feature area: common (shared strings), dashboard, pricing, auth, chat, etc. Access nested keys with dot notation. To add a new locale, create a new message file, add the locale to the config, and translate all keys.

Locale-Aware Components

The boilerplate provides components that automatically handle locale-based URL routing:

LocaleLink

Use the LocaleLink component instead of Next.js Link for internal navigation. It automatically prepends the current locale to the URL path (e.g., /pricing becomes /fr-FR/pricing). This ensures consistent locale-aware routing throughout the application.

import { LocaleLink } from '@/components/locale-link'

<LocaleLink href="/pricing">{t('nav.pricing')}</LocaleLink>
// renders <a href="/fr-FR/pricing"> on a French route

Language Switcher

The language switcher component displays a dropdown with all supported locales (with flag emojis). When a user selects a different language, it preserves the current URL path and replaces only the locale segment. The selected locale is stored in a cookie for persistence across sessions.

Date & Number Formatting

Always use the getIntlLocale() function when formatting dates or numbers with the Intl API. This converts BCP 47 locale codes to the format expected by JavaScript's Intl constructors. For example, use new Date().toLocaleDateString(getIntlLocale(locale)) for date formatting and new Intl.NumberFormat(getIntlLocale(locale), { style: 'currency', currency: 'EUR' }) for currency formatting.

Adding a New Locale

Steps to add a new locale
  1. Add the BCP 47 code to the locales tuple in i18n/config.ts, plus an entry in localeNames and localeFlags.
  2. Add the locale→currency mapping to localeCurrencyMap in config/pricing.ts (SUPPORTED_CURRENCIES is derived from it automatically).
  3. Copy an existing message file (e.g. i18n/messages/en-US.json) to i18n/messages/<locale>.json and translate every value — keep the key structure identical.
  4. For multi-locale JSONB content (CMS pages, blog, changelog), add the new locale key to existing rows; reads fall back to defaultLocale when a key is missing.
  5. Run npm run test__tests__/i18n/key-parity.test.ts fails the build if the locale files don't have identical key sets.

SEO & Metadata

Localize page metadata in an async generateMetadata() using the same getTranslations(locale) function, and emit hreflang alternates so search engines index every locale. The dynamic app/sitemap.ts already enumerates each locale × route (including CMS pages); app/robots.ts gates indexing behind NEXT_PUBLIC_INDEXABLE.

export async function generateMetadata(
  { params }: { params: Promise<{ locale: string }> }
): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations(locale as Locale)
  return {
    title: t('pricing.metaTitle'),
    alternates: {
      languages: { 'fr-FR': '/fr-FR/pricing', 'fr-CH': '/fr-CH/pricing', 'en-US': '/en-US/pricing', 'en-CA': '/en-CA/pricing' },
    },
  }
}

Best Practices

Never use language conditionals for text

This creates technical debt and breaks when adding new locales.

Do Don't
Use t('key') for all text Hardcode text in components
Pass translations to client components Use async getTranslations in client
Use getIntlLocale() for Intl APIs Pass locale directly to Intl
Organize keys by feature/page Flat structure with all keys
Use parameters for dynamic values Concatenate strings