Internationalization
Full multi-language support with next-intl — English, Chinese, and Spanish out of the box, auto language detection, runtime switcher, and an i18n consistency check script.
Supported Languages
| Code | Language | Native name |
|---|---|---|
en | English | English |
zh | Chinese | 简体中文 |
es | Spanish | Español |
Configuration
// src/config/site.ts
export const siteConfig = {
locale: {
default: 'en',
supported: [
{ value: 'en', label: 'English', nativeName: 'English' },
{ value: 'zh', label: 'Chinese', nativeName: '简体中文' },
{ value: 'es', label: 'Spanish', nativeName: 'Español' },
],
},
}Translation Files
Translation keys live in src/locales/:
src/locales/
├── en.json
├── zh.json
└── es.jsonAll three files must stay in sync. Use the consistency check script to catch missing or mismatched keys:
npm run i18n:checkThe script reports any key present in one file but missing in another.
Adding a New Key
Always update all three locale files at the same time:
// en.json
{ "admin": { "products": { "title": "Products" } } }
// zh.json
{ "admin": { "products": { "title": "商品" } } }
// es.json
{ "admin": { "products": { "title": "Productos" } } }Usage in Components
Client Component
import { useTranslations } from 'next-intl'
export function LoginForm() {
const t = useTranslations('auth.login')
return (
<form>
<h1>{t('title')}</h1>
<input placeholder={t('email')} />
<input type="password" placeholder={t('password')} />
<button>{t('submit')}</button>
</form>
)
}Server Component
import { getTranslations } from 'next-intl/server'
export default async function Page() {
const t = await getTranslations('common')
return <h1>{t('welcome')}</h1>
}API / Service Layer
import { getTranslations } from 'next-intl/server'
const t = await getTranslations('api.admin')
return ApiResponse.adminError(t('username_exists'), 400)Interpolation
{ "greeting": "Hello, {name}!", "items": "You have {count} items" }t('greeting', { name: 'Jane' }) // "Hello, Jane!"
t('items', { count: 3 }) // "You have 3 items"Pluralization
{ "items": "{count, plural, =0 {No items} =1 {One item} other {# items}}" }t('items', { count: 0 }) // "No items"
t('items', { count: 1 }) // "One item"
t('items', { count: 5 }) // "5 items"Language Detection Priority
The server-side locale resolution follows a strict priority order:
| Priority | Source | Notes |
|---|---|---|
| 1 (highest) | siteConfig.locale.default | If set to a valid locale in src/config/site.ts, it always wins |
| 2 | locale cookie | Written when the user explicitly picks a language via the switcher |
| 3 | Accept-Language header | Browser's preferred language sent on every request |
| 4 (fallback) | First supported locale | Last resort when all other sources are unavailable |
To lock the site to a specific language regardless of the user's browser or cookie, set locale.default in src/config/site.ts:
export const siteConfig = {
locale: {
default: 'en', // always serve English — set to '' to respect cookie / Accept-Language
...
},
}Leave default as an empty string ('') to fall back to cookie → Accept-Language detection.
Language Switcher
A built-in LanguageSelector component is available for placing anywhere in the UI:
import { LanguageSelector } from '@/components/locale/LanguageSelector'
<LanguageSelector />Selecting a language immediately updates the locale and reloads the page to apply changes.
Date, Number & Relative Time Formatting
import { useFormatter } from 'next-intl'
const format = useFormatter()
format.dateTime(date, { year: 'numeric', month: 'long', day: 'numeric' })
// en: "March 10, 2026" | zh: "2026年3月10日"
format.number(1234.56, { style: 'currency', currency: 'USD' })
// "$1,234.56"
format.relativeTime(pastDate)
// "2 months ago"Adding a New Language
- Copy an existing locale file:
cp src/locales/en.json src/locales/fr.json - Translate all values in
fr.json - Add the locale to
siteConfig.locale.supportedinsrc/config/site.ts - Add
'fr'to thelocalesarray insrc/lib/i18n/config.ts - Run
npm run i18n:checkto verify completeness
Namespace Conventions
Organize keys by module to keep namespaces focused:
| Namespace | Used for |
|---|---|
common | Shared labels (Save, Cancel, etc.) |
auth.login | Login page |
auth.register | Registration page |
admin.layout | Admin sidebar and header |
admin.users | Admin user management |
admin.products | Product management |
api.admin | Admin API error messages |
Never write bare English or Chinese strings in UI components. All visible text must use t('key').
Next Steps
- SEO — multi-language
hreflangalternates - Admin Panel — admin panel i18n conventions
Database
Multi-database support via Prisma ORM — PostgreSQL, MySQL, and SQLite. Switch providers with one command, type-safe queries, and automated setup.
UI & Theming
TailwindCSS 4 + DaisyUI 5 with 33 built-in themes, light/dark toggle, a full component library, Storybook 10 docs, and Recharts dashboard charts.