Skip to content

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

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

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

Conversation

CopilotAI commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

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

functiongetPreferredLanguage(request: NextRequest): string{// Cookie first, then Accept-Language headerconstcookieLocale=request.cookies.get('FD_LOCALE')?.value;if(cookieLocale&&SUPPORTED_LANGUAGES.includes(cookieLocale)){returncookieLocale;}constnegotiator=newNegotiator({headers: Object.fromEntries(request.headers.entries())});constbrowserLanguages=negotiator.languages().map(normalizeLanguage);returnbrowserLanguages.find(lang=>SUPPORTED_LANGUAGES.includes(lang))||i18n.defaultLanguage;}

Behavior

Browser LanguageURL VisitedResult
en-US/Stays on / (rewritten to /en internally)
zh-CN/Redirects to /cn
Any/enRedirects 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

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown

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

ProjectDeploymentReviewUpdated (UTC)
specReadyReadyPreview, CommentJan 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>
CopilotAI changed the title [WIP] Add automatic language redirection for homepagefeat: implement browser language detection and auto-redirect for homepageJan 19, 2026
@github-actionsgithub-actionsBot 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
CopilotAI review requested due to automatic review settings January 20, 2026 03:02
@hotlong
hotlong merged commit 2298e72 into mainJan 20, 2026
13 checks passed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
apps/docs/middleware.tsReplaced fumadocs default middleware with custom implementation for language detection, normalization, and routing
apps/docs/package.jsonAdded negotiator and @types/negotiator dependencies for Accept-Language parsing
pnpm-lock.yamlUpdated 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 });

CopilotAIJan 20, 2026

Copy link

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];

CopilotAIJan 20, 2026

Copy link

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
constlocale=pathname.split('/')[1];
constlocale=pathname.split('/')[1]||'';
// If the extracted locale segment is empty or invalid, skip locale-specific handling
if(!locale){
returnNextResponse.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/"

CopilotAIJan 20, 2026

Copy link

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
*-Forotherlanguages(cn): redirectsto"/cn/"
*-Forotherlanguages(cn): redirectsto"/cn"

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

CopilotAIJan 20, 2026

Copy link

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();

CopilotAIJan 20, 2026

Copy link

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
returnNextResponse.next();
constresponse=NextResponse.next();
setLocaleCookie(response,locale);
returnresponse;

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') || '/';

CopilotAIJan 20, 2026

Copy link

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(newRegExp(`^/${i18n.defaultLanguage}(/|$)`),'$1')||'/';
// Remove default locale prefix using string operations to avoid regex issues
constdefaultPrefix=`/${i18n.defaultLanguage}`;
if(pathname===defaultPrefix){
url.pathname='/';
}elseif(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);

CopilotAIJan 20, 2026

Copy link

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
returnNextResponse.rewrite(url);
constresponse=NextResponse.rewrite(url);
setLocaleCookie(response,preferredLanguage);
returnresponse;

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",

CopilotAIJan 20, 2026

Copy link

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 });

CopilotAIJan 20, 2026

Copy link

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
constnegotiatorHeaders=Object.fromEntries(request.headers.entries());
constnegotiator=newNegotiator({headers: negotiatorHeaders});
constacceptLanguage=request.headers.get('accept-language');
if(!acceptLanguage){
returni18n.defaultLanguage;
}
constnegotiator=newNegotiator({
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: '/',

CopilotAIJan 20, 2026

Copy link

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.
xuyushun441-sys pushed a commit that referenced this pull request May 25, 2026
Adds entries 26-40 covering the gaps that make the helpdesk template
'pretty but not daily-usable' from an end-user perspective:
P0 additions:
- #26 No inline message composer on detail pages
- #27 No external-user portal mechanism
- #28 Attachment/file-list field UI not E2E
P1 additions:
- #29 No 'changed since last visit' indicator
- #30 Bulk operations UI unverified (escalates #17)
- #31 Rich-text editor scoped to comments only
- #32 No first-class canned response / macro
- #33 No collaboration presence indicators
- #34 No keyboard-shortcut API
- #35 No conditional SLA timer (pause on waiting_customer)
- #36 Formula fields can't reference foreign object fields
P2 additions:
- #37 No chart drill-down
- #38 No period-over-period analytics primitive
- #39 No inbound-channel abstraction (email-to-ticket etc.)
- #40 i18n translation namespace validation weak
Includes 'user-pain → platform-gap' mapping table tracing each end-user
complaint to a specific issue number.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@huangyiirene@hotlong