Internationalization
Configure UI messages, locale-aware routes, and bilingual Fumadocs content with i18next.
The template uses i18next + react-i18next, not next-intl. Message lookup and URL routing are separate but coordinated: i18next loads UI strings, while an optional TanStack Router segment controls the public locale URL.
Shipped routing model
| Locale | URL form | Example |
|---|---|---|
English (en, default) | No prefix | /blog, /docs |
Chinese (zh) | /zh prefix | /zh/blog, /zh/docs |
src/routes/{-$locale}.tsx validates the optional segment in beforeLoad.
Unknown values return not found, /en/* redirects to the unprefixed canonical
path, and valid routes call i18n.changeLanguage(locale) before rendering. A
layout effect then updates <html lang> as navigation changes.
Despite older project notes saying there is no URL-segment routing, the
current template does have the optional {-$locale} segment. The default
locale is simply hidden from canonical URLs.
Important files
| File | Role |
|---|---|
src/i18n/index.ts | i18next instance, dictionaries, defaultLocale, supportedLocales, labels |
src/i18n/routing.tsx | Parse/strip/add locale prefixes, LocaleLink, current-locale and navigation hooks |
src/routes/{-$locale}.tsx | Segment validation, canonical redirect, language synchronization, document language |
src/components/common/LanguageToggle.tsx | Locale chooser |
messages/en.json, messages/zh.json | UI translations |
src/lib/source.ts | Fumadocs language configuration and localized docs URLs |
content/docs | Bilingual MDX pages |
localizePath() preserves query strings and hashes, removes any existing locale
prefix, and adds a prefix only for non-default locales. Prefer LocaleLink for
links and useLocaleRouter() for imperative navigation.
import { LocaleLink } from "#/i18n/routing"
<LocaleLink href="/pricing">Pricing</LocaleLink>Locale detection and switching
The shipped detectInitialLocale() reads the first browser pathname segment;
SSR defaults to English. The route layout is authoritative and synchronizes
i18next on navigation. The current LanguageToggle changes i18next and then
navigates to the selected locale's homepage.
i18next-browser-languagedetector is installed but is not initialized in the
current template. There is no shipped language cookie or local-storage
preference. If you add preference detection, keep explicit URL segments
authoritative and avoid redirect loops. Also change LanguageToggle if a
locale switch should preserve the current path instead of returning home.
Translate UI copy
Add the same nested key to both dictionaries, then use useTranslation() in a
component:
import { useTranslation } from "react-i18next"
export function Greeting() {
const { t } = useTranslation()
return <h1>{t("home.hero.title")}</h1>
}Keep dictionary shapes aligned. For type-safe or domain-specific namespaces, extend the i18next setup rather than embedding locale branches throughout UI.
Add another locale
Create the dictionary
Add messages/ja.json with the same keys as the existing dictionaries, then
import it into src/i18n/index.ts.
Extend locale configuration
Add "ja" to supportedLocales, add its localeLabels entry, and register its
resource:
export const supportedLocales = ["en", "zh", "ja"] as const
resources: {
en: { translation: en },
zh: { translation: zh },
ja: { translation: ja },
}If the new default should be different, update defaultLocale and retest every
canonical redirect.
Extend SEO locale mapping
Update toOpenGraphLocale() in src/lib/seo.ts. supportedLocales already
drives alternate links and sitemap expansion, but the Open Graph locale needs
the provider-specific value.
Add localized content
Fumadocs uses paired filenames: the default page is slug.mdx, Chinese is
slug.zh.mdx, so add slug.ja.mdx for Japanese. Do the same for every required
docs page and check navigation metadata. Blog and legal collection localization
should be designed explicitly before launch because their loaders currently
share content across locale URLs.
Verify
-
/and another English page render without/en. -
/en/blog?x=1redirects to/blog?x=1;/zh/blogremains Chinese. - An unsupported first segment is not mistaken for a locale.
- Locale-aware links retain query strings and hashes.
-
<html lang>changes after client navigation. - Every docs page has the expected localized filename and renders under its locale URL.
- Canonical, alternate, Open Graph, and sitemap URLs include the new locale correctly.
