Skip to main content
ShipFree is designed with internationalization (i18n) in mind, though the full next-intl integration is optional. The architecture supports multiple languages through locale-based routing and translation management.
While the boilerplate includes the structure for i18n, you can choose to implement it based on your needs. The default setup supports English, French, and Spanish.

Supported Languages

English

Default language (en)

French

French translations (fr)

Spanish

Spanish translations (es)

Architecture Overview

ShipFree uses locale-based routing where all pages are under a [locale] dynamic segment:
src/app/
└── [locale]/          # Dynamic locale segment
    ├── (auth)/        # Authentication pages
    ├── (main)/        # Main app pages
    └── (site)/        # Marketing pages
The locale is extracted from the URL path (e.g., /en/dashboard, /fr/dashboard, /es/dashboard).

Configuration

When implementing next-intl, you’ll typically have these configuration files:

Routing Configuration

i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
import { createNavigation } from 'next-intl/navigation'

export const routing = defineRouting({
  // Supported locales
  locales: ['en', 'fr', 'es'],
  
  // Default locale
  defaultLocale: 'en',
  
  // Locale detection
  localeDetection: true,
  
  // Locale prefix strategy
  localePrefix: 'always', // or 'as-needed', 'never'
})

export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)

Request Configuration

i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale
  
  // Validate locale
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale
  }
  
  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  }
})

Translation Files

Translations are stored as JSON files in src/messages/:
src/messages/en.json
{
  "common": {
    "welcome": "Welcome",
    "getStarted": "Get Started",
    "learnMore": "Learn More"
  },
  "auth": {
    "login": "Log In",
    "signup": "Sign Up",
    "logout": "Log Out",
    "email": "Email Address",
    "password": "Password"
  },
  "dashboard": {
    "title": "Dashboard",
    "overview": "Overview",
    "settings": "Settings"
  }
}

Using Translations

In Server Components

src/app/[locale]/(site)/page.tsx
import { getTranslations } from 'next-intl/server'

export default async function HomePage() {
  const t = await getTranslations('common')
  
  return (
    <div>
      <h1>{t('welcome')}</h1>
      <button>{t('getStarted')}</button>
    </div>
  )
}
In Server Components, use getTranslations from next-intl/server with await.

In Client Components

src/components/navbar.tsx
'use client'

import { useTranslations } from 'next-intl'

export function Navbar() {
  const t = useTranslations('common')
  
  return (
    <nav>
      <a href="/">{t('home')}</a>
      <a href="/about">{t('about')}</a>
      <a href="/contact">{t('contact')}</a>
    </nav>
  )
}
In Client Components, use the useTranslations hook without await.

With Parameters

import { useTranslations } from 'next-intl'

export function Greeting({ name }: { name: string }) {
  const t = useTranslations('greetings')
  
  return <p>{t('hello', { name })}</p>
}
src/messages/en.json
{
  "greetings": {
    "hello": "Hello, {name}!"
  }
}

Rich Text & Formatting

import { useTranslations } from 'next-intl'

export function RichTextExample() {
  const t = useTranslations('content')
  
  return (
    <div>
      {t.rich('description', {
        strong: (chunks) => <strong>{chunks}</strong>,
        link: (chunks) => <a href="/learn">{chunks}</a>,
      })}
    </div>
  )
}
src/messages/en.json
{
  "content": {
    "description": "This is <strong>important</strong>. <link>Learn more</link>"
  }
}

Locale-Aware Navigation

import { Link } from '@/i18n/routing'

export function Navigation() {
  return (
    <nav>
      <Link href="/dashboard">Dashboard</Link>
      <Link href="/settings">Settings</Link>
    </nav>
  )
}
The Link component from @/i18n/routing automatically prefixes URLs with the current locale.

Programmatic Navigation

'use client'

import { useRouter } from '@/i18n/routing'

export function NavigateButton() {
  const router = useRouter()
  
  const handleClick = () => {
    router.push('/dashboard')
  }
  
  return <button onClick={handleClick}>Go to Dashboard</button>
}

Locale Switcher

'use client'

import { useLocale } from 'next-intl'
import { useRouter, usePathname } from '@/i18n/routing'

export function LocaleSwitcher() {
  const locale = useLocale()
  const router = useRouter()
  const pathname = usePathname()
  
  const changeLocale = (newLocale: string) => {
    router.replace(pathname, { locale: newLocale })
  }
  
  return (
    <select value={locale} onChange={(e) => changeLocale(e.target.value)}>
      <option value="en">English</option>
      <option value="fr">Français</option>
      <option value="es">Español</option>
    </select>
  )
}

Metadata & SEO

src/app/[locale]/(site)/layout.tsx
import { getTranslations } from 'next-intl/server'

export async function generateMetadata({ 
  params: { locale } 
}: { 
  params: { locale: string } 
}) {
  const t = await getTranslations({ locale, namespace: 'metadata' })
  
  return {
    title: t('title'),
    description: t('description'),
  }
}

Date & Number Formatting

Dates

import { useFormatter } from 'next-intl'

export function DateDisplay({ date }: { date: Date }) {
  const format = useFormatter()
  
  return (
    <time dateTime={date.toISOString()}>
      {format.dateTime(date, {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      })}
    </time>
  )
}

Numbers & Currency

import { useFormatter } from 'next-intl'

export function PriceDisplay({ amount }: { amount: number }) {
  const format = useFormatter()
  
  return (
    <span>
      {format.number(amount, {
        style: 'currency',
        currency: 'USD',
      })}
    </span>
  )
}

Adding a New Language

1

Create Translation File

Add a new JSON file in src/messages/ (e.g., de.json for German):
src/messages/de.json
{
  "common": {
    "welcome": "Willkommen",
    "getStarted": "Loslegen"
  }
}
2

Update Routing Config

Add the locale to your routing configuration:
i18n/routing.ts
export const routing = defineRouting({
  locales: ['en', 'fr', 'es', 'de'], // Added 'de'
  defaultLocale: 'en',
})
3

Update Locale Switcher

Add the new language option to your locale switcher component.

Next.js Middleware

Configure middleware to handle locale detection:
middleware.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'

export default createMiddleware(routing)

export const config = {
  // Match all pathnames except for
  // - /api routes
  // - /_next (Next.js internals)
  // - /static files
  matcher: ['/((?!api|_next|.*\\..*).*)'],
}

Best Practices

Namespace Keys

Organize translations by feature or page for easier maintenance

Use TypeScript

Generate types from translation files for autocomplete

Pluralization

Use next-intl’s pluralization features for count-based text

SEO Optimization

Set proper lang attribute and hreflang tags for each locale

Translation Keys Type Safety

Generate TypeScript types from your translation files:
// Type-safe translations
const t = useTranslations('auth')
t('login')    // ✅ Valid
// t('invalid') // ❌ TypeScript error

Environment Variables

.env
NEXT_PUBLIC_DEFAULT_LOCALE=en
NEXT_PUBLIC_SUPPORTED_LOCALES=en,fr,es

Common Patterns

Conditional Rendering

import { useLocale } from 'next-intl'

export function FeatureFlag() {
  const locale = useLocale()
  
  // Show feature only for certain locales
  if (locale === 'en') {
    return <div>English-only feature</div>
  }
  
  return null
}

Server Actions

src/app/actions/user.ts
'use server'

import { getTranslations } from 'next-intl/server'

export async function updateProfile(locale: string, data: FormData) {
  const t = await getTranslations({ locale, namespace: 'messages' })
  
  try {
    // Update logic...
    return { success: true, message: t('updateSuccess') }
  } catch (error) {
    return { success: false, message: t('updateError') }
  }
}

Further Reading

next-intl Docs

Official next-intl documentation

Routing Guide

Learn about locale-based routing

Formatting

Date, time, and number formatting

Best Practices

i18n best practices and workflows