Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8
feat: implement browser language detection and auto-redirect for homepage#30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c74ab5a04d3893427d33ed0b61272725439File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,15 +1,135 @@ | ||||||||||||||||||||||||||
| import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware'; | ||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||
| import Negotiator from 'negotiator'; | ||||||||||||||||||||||||||
| import { i18n } from '@/lib/i18n'; | ||||||||||||||||||||||||||
| const LOCALE_COOKIE = 'FD_LOCALE'; | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Supported languages extracted from i18n configuration | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| const SUPPORTED_LANGUAGES = i18n.languages as readonly string[]; | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Set locale cookie with consistent options | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function setLocaleCookie(response: NextResponse, locale: string): void { | ||||||||||||||||||||||||||
| response.cookies.set(LOCALE_COOKIE, locale, { | ||||||||||||||||||||||||||
| sameSite: 'lax', | ||||||||||||||||||||||||||
| path: '/', | ||||||||||||||||||||||||||
CopilotAI | ||||||||||||||||||||||||||
| path: '/', | |
| path: '/', | |
| secure: process.env.NODE_ENV==='production', |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| 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}, | |
| }); |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| *-Forotherlanguages(cn): redirectsto"/cn/" | |
| *-Forotherlanguages(cn): redirectsto"/cn" |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| 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(); | |
| } |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| // 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||'/'; | |
| } |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| returnNextResponse.next(); | |
| constresponse=NextResponse.next(); | |
| setLocaleCookie(response,locale); | |
| returnresponse; |
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.
| returnNextResponse.rewrite(url); | |
| constresponse=NextResponse.rewrite(url); | |
| setLocaleCookie(response,preferredLanguage); | |
| returnresponse; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -23,10 +23,12 @@ | ||
| "devDependencies": { | ||
| "@tailwindcss/postcss": "^4.1.18", | ||
| "@tailwindcss/typography": "^0.5.19", | ||
| "@types/negotiator": "^0.6.4", | ||
| "@types/node": "^20.10.0", | ||
| "@types/react": "^19.2.8", | ||
| "@types/react-dom": "^19.2.3", | ||
| "autoprefixer": "^10.4.23", | ||
| "negotiator": "^1.0.0", | ||
CopilotAI | ||
| "postcss": "^8.5.6", | ||
| "tailwindcss": "^4.0.0", | ||
| "typescript": "^5.3.0", | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
CopilotAIJan 20, 2026
There was a problem hiding this comment.
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.