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
| File | Role |
|---|---|
src/i18n/routing.ts | Locale list, default locale, prefix strategy |
src/i18n/request.ts | Loads the message catalog per request |
src/i18n/navigation.ts | Locale-aware Link, redirect, useRouter |
src/proxy.ts | Locale detection + redirect middleware |
messages/en.json, messages/zh.json | Translation catalogs |
Routing
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:
| Locale | URL |
|---|---|
| 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:
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:
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:
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.
