Skip to content

Commit 04d3893

Browse files
Copilothuangyiirene
andcommitted
feat: implement language-based homepage redirection
- Add custom middleware for automatic language detection - Map Chinese language codes (zh, zh-CN, zh-TW, zh-HK) to 'cn' - Redirect non-default language users to localized paths (/cn/) - Keep default language (en) users on clean URLs (/) - Store language preference in cookies for consistency - Add negotiator and type definitions as dependencies Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent c74ab5a commit 04d3893

3 files changed

Lines changed: 140 additions & 2 deletions

File tree

apps/docs/middleware.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,121 @@
1-
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import Negotiator from 'negotiator';
23
import { i18n } from '@/lib/i18n';
34

5+
const LOCALE_COOKIE = 'FD_LOCALE';
6+
7+
/**
8+
* Language code mapping
9+
* Maps browser language codes to our supported language codes
10+
*/
11+
const LANGUAGE_MAPPING: Record<string, string> = {
12+
'zh': 'cn', // Chinese -> cn
13+
'zh-CN': 'cn', // Chinese (China) -> cn
14+
'zh-TW': 'cn', // Chinese (Taiwan) -> cn
15+
'zh-HK': 'cn', // Chinese (Hong Kong) -> cn
16+
};
17+
18+
/**
19+
* Normalize language code to match our supported languages
20+
*/
21+
function normalizeLanguage(lang: string): string {
22+
// Check direct mapping first
23+
if (LANGUAGE_MAPPING[lang]) {
24+
return LANGUAGE_MAPPING[lang];
25+
}
26+
27+
// Check if the base language (without region) is mapped
28+
const baseLang = lang.split('-')[0];
29+
if (LANGUAGE_MAPPING[baseLang]) {
30+
return LANGUAGE_MAPPING[baseLang];
31+
}
32+
33+
return lang;
34+
}
35+
36+
/**
37+
* Get the preferred language from the request
38+
*/
39+
function getPreferredLanguage(request: NextRequest): string {
40+
// Check cookie first
41+
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
42+
if (cookieLocale && (i18n.languages as readonly string[]).includes(cookieLocale)) {
43+
return cookieLocale;
44+
}
45+
46+
// Then check Accept-Language header
47+
const negotiatorHeaders: Record<string, string> = {};
48+
request.headers.forEach((value, key) => {
49+
negotiatorHeaders[key] = value;
50+
});
51+
52+
const negotiator = new Negotiator({ headers: negotiatorHeaders });
53+
const browserLanguages = negotiator.languages();
54+
55+
// Normalize browser languages to match our supported languages
56+
const normalizedLanguages = browserLanguages.map(normalizeLanguage);
57+
58+
// Find the first match
59+
for (const lang of normalizedLanguages) {
60+
if ((i18n.languages as readonly string[]).includes(lang)) {
61+
return lang;
62+
}
63+
}
64+
65+
return i18n.defaultLanguage;
66+
}
67+
468
/**
569
* Middleware for automatic language detection and redirection
670
*
771
* This middleware:
872
* - Detects the user's preferred language from browser settings or cookies
973
* - Redirects users to the appropriate localized version
74+
* - For default language (en): keeps URL as "/" (with internal rewrite)
75+
* - For other languages (cn): redirects to "/cn/"
1076
* - Stores language preference as a cookie
1177
*/
12-
export default createI18nMiddleware(i18n);
78+
export default function middleware(request: NextRequest) {
79+
const { pathname } = request.nextUrl;
80+
81+
// Check if the pathname already has a locale
82+
const pathnameHasLocale = i18n.languages.some(
83+
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
84+
);
85+
86+
if (pathnameHasLocale) {
87+
// Extract the locale from the pathname
88+
const locale = pathname.split('/')[1];
89+
90+
// If it's the default locale and hideLocale is 'default-locale', redirect to remove locale prefix
91+
if (locale === i18n.defaultLanguage && i18n.hideLocale === 'default-locale') {
92+
const url = new URL(request.url);
93+
url.pathname = pathname.replace(`/${i18n.defaultLanguage}`, '') || '/';
94+
const response = NextResponse.redirect(url);
95+
response.cookies.set(LOCALE_COOKIE, locale);
96+
return response;
97+
}
98+
99+
return NextResponse.next();
100+
}
101+
102+
// Pathname doesn't have a locale, determine preferred language
103+
const preferredLanguage = getPreferredLanguage(request);
104+
105+
// If preferred language is the default, rewrite internally (keep URL clean)
106+
if (preferredLanguage === i18n.defaultLanguage && i18n.hideLocale === 'default-locale') {
107+
const url = new URL(request.url);
108+
url.pathname = `/${i18n.defaultLanguage}${pathname}`;
109+
return NextResponse.rewrite(url);
110+
}
111+
112+
// For non-default languages, redirect to the localized path
113+
const url = new URL(request.url);
114+
url.pathname = `/${preferredLanguage}${pathname}`;
115+
const response = NextResponse.redirect(url);
116+
response.cookies.set(LOCALE_COOKIE, preferredLanguage);
117+
return response;
118+
}
13119

14120
export const config = {
15121
// Match all routes except:

apps/docs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
"server-only": "^0.0.1"
2222
},
2323
"devDependencies": {
24+
"@formatjs/intl-localematcher": "^0.8.0",
2425
"@tailwindcss/postcss": "^4.1.18",
2526
"@tailwindcss/typography": "^0.5.19",
27+
"@types/negotiator": "^0.6.4",
2628
"@types/node": "^20.10.0",
2729
"@types/react": "^19.2.8",
2830
"@types/react-dom": "^19.2.3",
2931
"autoprefixer": "^10.4.23",
32+
"negotiator": "^1.0.0",
3033
"postcss": "^8.5.6",
3134
"tailwindcss": "^4.0.0",
3235
"typescript": "^5.3.0",

pnpm-lock.yaml

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)