Skip to content

feat: implement browser language detection and auto-redirect for homepage#30

Merged
hotlong merged 5 commits intomainfrom
copilot/add-language-auto-redirect
Jan 20, 2026
Merged

feat: implement browser language detection and auto-redirect for homepage#30
hotlong merged 5 commits intomainfrom
copilot/add-language-auto-redirect

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 19, 2026

The homepage now redirects users to their language-preferred interface based on browser Accept-Language headers. Chinese users are redirected to /cn, while English users remain on / (the default).

Changes

Custom Middleware (apps/docs/middleware.ts)

  • Replaced fumadocs default middleware with custom implementation
  • Maps Chinese language codes (zh, zh-CN, zh-TW, zh-HK) → cn
  • English (default): Internal rewrite to /en, URL stays /
  • Non-default languages: HTTP 307 redirect to localized path + cookie persistence
  • Cookie options: sameSite: 'lax', path: '/'

Dependencies (apps/docs/package.json)

  • Added negotiator for Accept-Language parsing
  • Added @types/negotiator

Implementation

function getPreferredLanguage(request: NextRequest): string {
  // Cookie first, then Accept-Language header
  const cookieLocale = request.cookies.get('FD_LOCALE')?.value;
  if (cookieLocale && SUPPORTED_LANGUAGES.includes(cookieLocale)) {
    return cookieLocale;
  }

  const negotiator = new Negotiator({ headers: Object.fromEntries(request.headers.entries()) });
  const browserLanguages = negotiator.languages().map(normalizeLanguage);
  
  return browserLanguages.find(lang => SUPPORTED_LANGUAGES.includes(lang)) 
    || i18n.defaultLanguage;
}

Behavior

Browser Language URL Visited Result
en-US / Stays on / (rewritten to /en internally)
zh-CN / Redirects to /cn
Any /en Redirects to / (strips default locale)

Screenshots

English Homepage (/)
English

Chinese Homepage (/cn)
Chinese

Original prompt

首页应根据当前的语言自动跳转到对应的语言界面


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 19, 2026 3:21pm

Request Review

- 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>
- Remove unused @formatjs/intl-localematcher dependency
- Extract SUPPORTED_LANGUAGES constant to reduce type assertion duplication
- Use Object.fromEntries for more concise header conversion
- Improve code readability and efficiency

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
- Add setLocaleCookie helper function with consistent cookie options
- Set sameSite=lax and path=/ for security and consistency
- Update comment for SUPPORTED_LANGUAGES constant
- Ensure all cookie operations use the same configuration

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
- Handle root path (/) specially to avoid double slashes
- Use precise regex for locale prefix removal
- Ensure clean URLs: /cn instead of /cn/
- Improve path normalization logic

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Copilot AI changed the title [WIP] Add automatic language redirection for homepage feat: implement browser language detection and auto-redirect for homepage Jan 19, 2026
Copilot AI requested a review from huangyiirene January 19, 2026 15:23
@github-actions github-actions bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file size/m labels Jan 20, 2026
@hotlong hotlong marked this pull request as ready for review January 20, 2026 03:02
Copilot AI review requested due to automatic review settings January 20, 2026 03:02
@hotlong hotlong merged commit 2298e72 into main Jan 20, 2026
13 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements browser language detection and automatic redirection for the homepage, replacing the default fumadocs i18n middleware with a custom implementation that maps Chinese language codes to /cn while keeping English users on / through internal rewrites.

Changes:

  • Custom middleware with Accept-Language header parsing using the negotiator library
  • Cookie-based language preference persistence with FD_LOCALE cookie
  • Locale-specific redirection logic: Chinese variants (zh, zh-CN, zh-TW, zh-HK) redirect to /cn, English stays on / with internal rewrite to /en

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 10 comments.

File Description
apps/docs/middleware.ts Replaced fumadocs default middleware with custom implementation for language detection, normalization, and routing
apps/docs/package.json Added negotiator and @types/negotiator dependencies for Accept-Language parsing
pnpm-lock.yaml Updated lockfile with new negotiator package and its type definitions
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +54 to +63
function getPreferredLanguage(request: NextRequest): string {
// Check cookie first
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
if (cookieLocale && SUPPORTED_LANGUAGES.includes(cookieLocale)) {
return cookieLocale;
}

// Then check Accept-Language header
const negotiatorHeaders = Object.fromEntries(request.headers.entries());
const negotiator = new Negotiator({ headers: negotiatorHeaders });
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware creates a new Negotiator instance on every request even when a cookie is already set and valid. Consider skipping the Negotiator initialization if the cookie check at lines 56-59 returns early, by restructuring the code to only create the Negotiator instance after the cookie check fails.

Copilot uses AI. Check for mistakes.

if (pathnameHasLocale) {
// Extract the locale from the pathname
const locale = pathname.split('/')[1];
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware doesn't handle the case where pathname.split('/')[1] could be an empty string. If a pathname like //something is provided, locale would be an empty string, which could cause unexpected behavior. Consider adding validation to ensure the extracted locale is a non-empty string before using it.

Suggested change
const locale = pathname.split('/')[1];
const locale = pathname.split('/')[1] || '';
// If the extracted locale segment is empty or invalid, skip locale-specific handling
if (!locale) {
return NextResponse.next();
}

Copilot uses AI. Check for mistakes.
* - Detects the user's preferred language from browser settings or cookies
* - Redirects users to the appropriate localized version
* - For default language (en): keeps URL as "/" (with internal rewrite)
* - For other languages (cn): redirects to "/cn/"
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "redirects to '/cn/'" (with trailing slash) but the actual implementation redirects to '/cn' (without trailing slash). The documentation should be corrected to accurately reflect the implementation.

Suggested change
* - For other languages (cn): redirects to "/cn/"
* - For other languages (cn): redirects to "/cn"

Copilot uses AI. Check for mistakes.
function setLocaleCookie(response: NextResponse, locale: string): void {
response.cookies.set(LOCALE_COOKIE, locale, {
sameSite: 'lax',
path: '/',
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cookie is being set without an expiration time. This means it will be a session cookie that gets deleted when the browser closes. For a language preference, it would be better to set an explicit expiration (e.g., 1 year) so the preference persists across browser sessions.

Suggested change
path: '/',
path: '/',
// Persist language preference across browser sessions (1 year)
maxAge: 60 * 60 * 24 * 365,

Copilot uses AI. Check for mistakes.
return response;
}

return NextResponse.next();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the pathname has a locale and it's not the default locale, the cookie is not being set. This means if a user manually navigates to a non-default locale URL (e.g., /cn), their preference won't be stored in the cookie. Consider adding a response with the cookie set in the else branch at line 111.

Suggested change
return NextResponse.next();
const response = NextResponse.next();
setLocaleCookie(response, locale);
return response;

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +105
// Remove locale prefix more precisely to avoid issues with partial matches
url.pathname = pathname.replace(new RegExp(`^/${i18n.defaultLanguage}(/|$)`), '$1') || '/';
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regular expression ^/${i18n.defaultLanguage}(/|$) could be vulnerable to ReDoS (Regular Expression Denial of Service) if i18n.defaultLanguage contains special regex characters. While the current value 'en' is safe, it would be safer to escape the language code using a helper function or use a simpler string-based approach. Consider using pathname.slice(i18n.defaultLanguage.length + 1) or escaping the language code.

Suggested change
// Remove locale prefix more precisely to avoid issues with partial matches
url.pathname = pathname.replace(new RegExp(`^/${i18n.defaultLanguage}(/|$)`), '$1') || '/';
// Remove default locale prefix using string operations to avoid regex issues
const defaultPrefix = `/${i18n.defaultLanguage}`;
if (pathname === defaultPrefix) {
url.pathname = '/';
} else if (pathname.startsWith(`${defaultPrefix}/`)) {
url.pathname = pathname.slice(defaultPrefix.length) || '/';
} else {
// Fallback: if for some reason the pathname doesn't match the expected pattern, keep it as is
url.pathname = pathname || '/';
}

Copilot uses AI. Check for mistakes.
const url = new URL(request.url);
// Handle root path specially to avoid double slashes
url.pathname = pathname === '/' ? `/${i18n.defaultLanguage}` : `/${i18n.defaultLanguage}${pathname}`;
return NextResponse.rewrite(url);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cookie is not being set when the user's preferred language matches the default language. This means if an English-speaking user visits the site, their preference won't be stored, and the language detection will run on every subsequent request instead of using the cached cookie value. Consider setting the cookie even for the default language to improve performance on subsequent requests.

Suggested change
return NextResponse.rewrite(url);
const response = NextResponse.rewrite(url);
setLocaleCookie(response, preferredLanguage);
return response;

Copilot uses AI. Check for mistakes.
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.23",
"negotiator": "^1.0.0",
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The negotiator package is listed as a devDependency but it's imported and used in the middleware which runs at runtime. This package should be moved to the dependencies section instead of devDependencies to ensure it's available in production builds.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +63
const negotiatorHeaders = Object.fromEntries(request.headers.entries());
const negotiator = new Negotiator({ headers: negotiatorHeaders });
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting headers to a plain object using Object.fromEntries(request.headers.entries()) could potentially cause issues if there are duplicate header names (though unlikely with Accept-Language). Additionally, this creates a new object on every call. Consider passing the headers directly if the Negotiator API supports it, or at least document why this conversion is necessary.

Suggested change
const negotiatorHeaders = Object.fromEntries(request.headers.entries());
const negotiator = new Negotiator({ headers: negotiatorHeaders });
const acceptLanguage = request.headers.get('accept-language');
if (!acceptLanguage) {
return i18n.defaultLanguage;
}
const negotiator = new Negotiator({
headers: { 'accept-language': acceptLanguage },
});

Copilot uses AI. Check for mistakes.
function setLocaleCookie(response: NextResponse, locale: string): void {
response.cookies.set(LOCALE_COOKIE, locale, {
sameSite: 'lax',
path: '/',
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cookie should also set the secure flag in production environments to ensure it's only transmitted over HTTPS. Consider adding secure: process.env.NODE_ENV === 'production' to the cookie options.

Suggested change
path: '/',
path: '/',
secure: process.env.NODE_ENV === 'production',

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants