Customization
Internationalization
Multi-language support with next-intl — add new locales, use translations, and switch languages at runtime.
How i18n Works
Haze Dashboard uses next-intl with a cookie-based locale strategy. URLs do not change based on locale — there is no /en/ or /de/prefix. The language is determined by the user's selection and stored in the NEXT_LOCALE cookie.
Locale Files
Three locales ship by default. Each JSON file contains ~100 translation keys organized by feature area:
| File | Language | Code |
|---|---|---|
| src/messages/en.json | English | en |
| src/messages/de.json | Deutsch (German) | de |
| src/messages/fr.json | Français (French) | fr |
Keys are organized in nested groups for easy navigation:
{
"app": {
"title": "Haze Dashboard",
"tagline": "Modern admin dashboard"
},
"nav": {
"dashboard": "Dashboard",
"analytics": "Analytics",
"orders": "Orders"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"search": "Search...",
"loading": "Loading..."
},
"status": {
"active": "Active",
"pending": "Pending",
"completed": "Completed"
},
"auth": {
"login": "Sign in",
"register": "Create account",
"forgot_password": "Forgot password?"
}
}Using Translations
Server components use getTranslations() from next-intl/server. Client components use the useTranslations() hook:
// In a server component
import { getTranslations } from 'next-intl/server'
export default async function DashboardPage() {
const t = await getTranslations()
return (
<>
<h1>{t('nav.dashboard')}</h1>
<p>{t('common.loading')}</p>
<span>{t('status.active')}</span>
</>
)
}
// In a client component
'use client'
import { useTranslations } from 'next-intl'
export function MyComponent() {
const t = useTranslations()
return <h1>{t('nav.dashboard')}</h1>
}Interpolation
Pass dynamic values to translations using curly braces in the JSON key and an object as the second argument:
{
"dashboard": {
"welcome": "Welcome back, {name}!",
"showing": "Showing {count} of {total} results"
}
}// In a component
const t = useTranslations()
<h1>{t('dashboard.welcome', { name: 'Alex' })}</h1>
// Output: Welcome back, Alex!
<p>{t('dashboard.showing', { count: 10, total: 248 })}</p>
// Output: Showing 10 of 248 resultsAdding a New Locale
To add Spanish support, follow these two steps:
1. Create the locale file — Copy src/messages/en.json to src/messages/es.json and translate all values:
// src/messages/es.json
{
"app": {
"title": "Haze Dashboard",
"tagline": "Panel de administracion moderno"
},
"nav": {
"dashboard": "Panel",
"analytics": "Analiticas",
"orders": "Pedidos"
},
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar"
}
}2. Register it in the i18n config — Add the new locale to the supported list:
// src/i18n.ts
import { getRequestConfig } from 'next-intl/server'
export const locales = ['en', 'de', 'fr', 'es'] as const // Add 'es'
export const defaultLocale = 'en'
export default getRequestConfig(async () => {
const locale = defaultLocale
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default,
}
})The locale switcher in the header will automatically include the new language.
Translation Key Conventions
| Prefix | Usage | Example |
|---|---|---|
| app.* | App-level labels (title, tagline) | app.title |
| nav.* | Navigation items | nav.dashboard |
| common.* | Shared action labels | common.save |
| status.* | Status labels for badges | status.pending |
| auth.* | Authentication forms | auth.login |
Locale Switcher
The header includes a dropdown that lets users switch languages at runtime. It sets the NEXT_LOCALE cookie and reloads the page:
'use client'
import { useLocale } from 'next-intl'
export function LocaleSwitcher() {
const locale = useLocale()
const locales = ['en', 'de', 'fr']
function changeLanguage(code: string) {
document.cookie = `NEXT_LOCALE=${code}; path=/; max-age=31536000`
window.location.reload()
}
return (
<select value={locale} onChange={e => changeLanguage(e.target.value)}>
{locales.map(l => (
<option key={l} value={l}>{l.toUpperCase()}</option>
))}
</select>
)
}Tip
Keep keys organized by feature area (app.*, nav.*, common.*, etc.). It makes finding and updating translations much easier as the app grows.
Next Steps
Explore the Components reference to see all available shared UI primitives.