Docs
Features

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

CodeLanguageNative name
enEnglishEnglish
zhChinese简体中文
esSpanishEspañ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.json

All three files must stay in sync. Use the consistency check script to catch missing or mismatched keys:

npm run i18n:check

The 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:

PrioritySourceNotes
1 (highest)siteConfig.locale.defaultIf set to a valid locale in src/config/site.ts, it always wins
2locale cookieWritten when the user explicitly picks a language via the switcher
3Accept-Language headerBrowser's preferred language sent on every request
4 (fallback)First supported localeLast 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

  1. Copy an existing locale file: cp src/locales/en.json src/locales/fr.json
  2. Translate all values in fr.json
  3. Add the locale to siteConfig.locale.supported in src/config/site.ts
  4. Add 'fr' to the locales array in src/lib/i18n/config.ts
  5. Run npm run i18n:check to verify completeness

Namespace Conventions

Organize keys by module to keep namespaces focused:

NamespaceUsed for
commonShared labels (Save, Cancel, etc.)
auth.loginLogin page
auth.registerRegistration page
admin.layoutAdmin sidebar and header
admin.usersAdmin user management
admin.productsProduct management
api.adminAdmin 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 hreflang alternates
  • Admin Panel — admin panel i18n conventions
Internationalization | Tikship