Internationalization

Configure locales and routing with next-intl.

The template ships bilingual (English + Chinese) using next-intl. Locale is part of the URL via the [locale] segment, messages are JSON catalogs, and src/proxy.ts handles detection and redirects.

Important files

FileRole
src/i18n/routing.tsLocale list, default locale, prefix strategy
src/i18n/request.tsLoads the message catalog per request
src/i18n/navigation.tsLocale-aware Link, redirect, useRouter
src/proxy.tsLocale detection + redirect middleware
messages/en.json, messages/zh.jsonTranslation catalogs

Routing

src/i18n/routing.ts
export const routing = defineRouting({
  locales: ["en", "zh"],
  defaultLocale: "en",
  localePrefix: "as-needed",
});

localePrefix: "as-needed" means the default locale is unprefixed and other locales carry their code:

LocaleURL
English (default)/, /about, /docs/next
Chinese/zh, /zh/about, /zh/docs/next

request.ts reads the resolved locale, falls back to the default for unknown values, and imports the matching messages/{locale}.json.

Using translations & navigation

In components, pull strings with next-intl's hooks/helpers (useTranslations, getTranslations) keyed against messages/*.json. For any internal navigation, import from src/i18n/navigation.ts rather than next/link so the active locale is preserved:

src/i18n/navigation.ts
export const { Link, redirect, usePathname, useRouter } =
  createNavigation(routing);

Detection & redirects: src/proxy.ts

proxy.ts runs next-intl's middleware first to detect and prefix the locale, then applies the protected-path check. Its matcher excludes API routes, Next internals, and files with extensions:

src/proxy.ts
export const config = {
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

API routes are skipped explicitly, so /api/* is never locale-prefixed. See Project Structure for the full request flow.

Adding a locale

Add the message catalog

Create messages/<locale>.json with the same keys as en.json.

Register it in routing

Add the code to locales in src/i18n/routing.ts. request.ts imports catalogs dynamically, so no change is needed there.

Translate UI config

Footer and other label sources go through the next-intl translator, so the new keys flow through automatically once the catalog has them.

Documentation locales (Fumadocs)

Docs are bilingual too, configured in src/lib/source.ts:

src/lib/source.ts
export const source = loader({
  baseUrl: "/docs",
  source: docs.toFumadocsSource(),
  i18n: { defaultLanguage: "en", languages: ["en", "zh"] },
});

Each doc ships as a filename pair: slug.mdx (English) and slug.zh.mdx (Chinese) — for the section index it's index.mdx / index.zh.mdx. Fumadocs resolves the right file from the request locale.

Keep the pair in sync: a slug listed in a docs meta.json must have both slug.mdx and slug.zh.mdx, or the nav link 404s for the locale that's missing.

On this page