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:

FileLanguageCode
src/messages/en.jsonEnglishen
src/messages/de.jsonDeutsch (German)de
src/messages/fr.jsonFranç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 results

Adding 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

PrefixUsageExample
app.*App-level labels (title, tagline)app.title
nav.*Navigation itemsnav.dashboard
common.*Shared action labelscommon.save
status.*Status labels for badgesstatus.pending
auth.*Authentication formsauth.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.