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
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
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/:
English (en.json)
French (fr.json)
Spanish (es.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"
}
}
{
"common" : {
"welcome" : "Bienvenue" ,
"getStarted" : "Commencer" ,
"learnMore" : "En savoir plus"
},
"auth" : {
"login" : "Se connecter" ,
"signup" : "S'inscrire" ,
"logout" : "Se déconnecter" ,
"email" : "Adresse e-mail" ,
"password" : "Mot de passe"
},
"dashboard" : {
"title" : "Tableau de bord" ,
"overview" : "Aperçu" ,
"settings" : "Paramètres"
}
}
{
"common" : {
"welcome" : "Bienvenido" ,
"getStarted" : "Comenzar" ,
"learnMore" : "Saber más"
},
"auth" : {
"login" : "Iniciar sesión" ,
"signup" : "Registrarse" ,
"logout" : "Cerrar sesión" ,
"email" : "Correo electrónico" ,
"password" : "Contraseña"
},
"dashboard" : {
"title" : "Panel" ,
"overview" : "Resumen" ,
"settings" : "Configuración"
}
}
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 >
}
{
"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 >
)
}
{
"content" : {
"description" : "This is <strong>important</strong>. <link>Learn more</link>"
}
}
Locale-Aware Navigation
Using Link Component
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 >
)
}
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' ),
}
}
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
Create Translation File
Add a new JSON file in src/messages/ (e.g., de.json for German): {
"common" : {
"welcome" : "Willkommen" ,
"getStarted" : "Loslegen"
}
}
Update Routing Config
Add the locale to your routing configuration: export const routing = defineRouting ({
locales: [ 'en' , 'fr' , 'es' , 'de' ], // Added 'de'
defaultLocale: 'en' ,
})
Update Locale Switcher
Add the new language option to your locale switcher component.
Next.js Middleware
Configure middleware to handle locale detection:
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
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
'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