fix(i18n): preserve locale across navigation and language switching#65
Conversation
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
WalkthroughThe changes implement locale-aware routing and internationalization improvements across the frontend. A new Claude settings file enables bash commands, layout components receive explicit locale parameters, the root HTML element declares Ukrainian language, locale-prefixed URL navigation is refactored, and middleware logic is updated to handle locale extraction from request paths and redirect authentication traffic appropriately. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
frontend/middleware.ts (1)
52-55: Consider locale selection strategy for root path.The middleware redirects the root path
/to/uk, effectively making Ukrainian the default locale for all users. While this is functionally correct and ensures a consistent experience, it may not be ideal for international users.Consider these alternatives:
- Browser language detection: Use the
Accept-Languageheader to redirect to the user's preferred locale (next-intl middleware can handle this automatically if configured).- Geolocation-based default: Determine locale based on the user's location.
- Remember user preference: Store the last-used locale in a cookie and redirect to that.
The current approach is valid if Ukrainian is intentionally the primary locale, but you may want to enable next-intl's automatic locale detection by adjusting the middleware configuration:
const intlMiddleware = createIntlMiddleware({ ...routing, localeDetection: true, // Enable automatic locale detection });This would allow next-intl to automatically detect and redirect based on the
Accept-Languageheader instead of always defaulting to/uk.frontend/components/shared/LanguageSwitcher.tsx (1)
15-16: Refactor locale pattern to use dynamic configuration.The hardcoded locale list
(uk|en|pl)appears in multiple locations (LanguageSwitcher.tsx line 16, middleware.ts lines 23 and 46, and the matcher config on line 72). While currently synchronized with the config, this creates maintenance risk—adding or removing locales requires manual updates in each location.The suggested refactor remains valid. For LanguageSwitcher.tsx specifically:
const fullPathname = usePathname(); const params = useParams(); const currentLocale = params.locale as Locale; - // Remove locale prefix from pathname: /uk/q&a -> /q&a, /uk -> / - const pathname = fullPathname.replace(/^\/(uk|en|pl)/, '') || '/'; + // Remove locale prefix from pathname: /uk/q&a -> /q&a, /uk -> / + const localePattern = new RegExp(`^/(${locales.join('|')})`); + const pathname = fullPathname.replace(localePattern, '') || '/';Similar refactoring should be applied to middleware.ts to keep all locale patterns in sync with the config.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
frontend/.claude/settings.local.json(1 hunks)frontend/app/[locale]/layout.tsx(1 hunks)frontend/app/layout.tsx(1 hunks)frontend/components/shared/LanguageSwitcher.tsx(1 hunks)frontend/middleware.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/components/shared/LanguageSwitcher.tsx (1)
frontend/i18n/config.ts (1)
Locale(2-2)
🔇 Additional comments (3)
frontend/app/[locale]/layout.tsx (1)
30-30: LGTM! Explicit locale parameter improves clarity.Passing the
localeparameter explicitly togetMessages({ locale })is a good improvement. This ensures that the message loading is directly tied to the locale extracted from the URL, making the code more explicit and maintainable.frontend/components/shared/LanguageSwitcher.tsx (1)
4-4: Minor: Import consolidation is good.Nice cleanup consolidating the imports from
next/navigationinto a single line.frontend/middleware.ts (1)
63-66: LGTM! Simplified header handling improves code clarity.The refactored header handling is cleaner and more straightforward. Always setting
x-app-scopeon theintlResponseand returning it directly eliminates unnecessary conditional logic.
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "Bash(grep:*)", | ||
| "Bash(Get-Process -Id 9420 -ErrorAction SilentlyContinue)", | ||
| "Bash(Stop-Process -Force)", | ||
| "Bash(taskkill //F //PID 9420)", | ||
| "Bash(npm run dev:*)", | ||
| "Bash(taskkill //F //PID 8344)" | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
Remove local configuration file from repository.
This file appears to be a local Claude Code editor configuration with machine-specific settings (specific PIDs like 9420 and 8344). It's unrelated to the i18n locale preservation functionality of this PR and shouldn't be committed to the repository.
Files with .local in the name are typically meant to be excluded from version control and listed in .gitignore. This file contains development environment settings that are specific to your local machine and won't be useful to other developers.
Please remove this file from the PR and ensure it's added to .gitignore:
+ # Claude local settings
+ .claude/settings.local.jsonCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
frontend/.claude/settings.local.json lines 1-12: this is a machine-specific
local Claude editor config (contains PIDs) that should not be committed; remove
the file from the PR and repository history (git rm --cached
frontend/.claude/settings.local.json or git rm
frontend/.claude/settings.local.json and commit), add an entry for
/.claude/*.local or specifically frontend/.claude/settings.local.json to
.gitignore, and commit the .gitignore change so the file is no longer tracked in
future commits.
| }>) { | ||
| return ( | ||
| <html suppressHydrationWarning> | ||
| <html lang="uk" suppressHydrationWarning> |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
# First, explore the frontend app structure
find frontend/app -type f -name "*.tsx" | head -20Repository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
frontend/app/[locale]/about/page.tsx
frontend/app/[locale]/blog/[slug]/PostDetails.tsx
frontend/app/[locale]/blog/[slug]/page.tsx
frontend/app/[locale]/blog/page.tsx
frontend/app/[locale]/contacts/page.tsx
frontend/app/[locale]/dashboard/page.tsx
frontend/app/[locale]/leaderboard/page.tsx
frontend/app/[locale]/login/page.tsx
frontend/app/[locale]/not-found.tsx
frontend/app/[locale]/page.tsx
frontend/app/[locale]/privacy-policy/page.tsx
frontend/app/[locale]/q&a/page.tsx
frontend/app/[locale]/quiz/[slug]/page.tsx
frontend/app/[locale]/shop/admin/layout.tsx
frontend/app/[locale]/shop/admin/orders/[id]/page.tsx
frontend/app/[locale]/shop/admin/orders/page.tsx
frontend/app/[locale]/shop/admin/page.tsx
frontend/app/[locale]/shop/admin/products/[id]/edit/page.tsx
frontend/app/[locale]/shop/admin/products/_components/product-form.tsx
frontend/app/[locale]/shop/admin/products/new/page.tsx
Script executed:
# Look at the root layout file
cat -n frontend/app/layout.tsxRepository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
1 import { Geist, Geist_Mono } from 'next/font/google';
2 import './globals.css';
3
4 const geistSans = Geist({
5 variable: '--font-geist-sans',
6 subsets: ['latin'],
7 });
8
9 const geistMono = Geist_Mono({
10 variable: '--font-geist-mono',
11 subsets: ['latin'],
12 });
13
14 export default function RootLayout({
15 children,
16 }: Readonly<{
17 children: React.ReactNode;
18 }>) {
19 return (
20 <html lang="uk" suppressHydrationWarning>
21 <body
22 suppressHydrationWarning
23 className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-50 text-gray-900 dark:bg-neutral-950 dark:text-gray-100 transition-colors duration-300`}
24 >
25 {children}
26 </body>
27 </html>
28 );
29 }
Script executed:
# Check if there's a locale-specific layout
cat -n frontend/app/\[locale\]/layout.tsxRepository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
1 import { Toaster } from 'sonner';
2 import { NextIntlClientProvider } from 'next-intl';
3 import { getMessages } from 'next-intl/server';
4 import { notFound } from 'next/navigation';
5 import { locales } from '@/i18n/config';
6 import { Link } from '@/i18n/routing';
7 import Footer from '@/components/shared/Footer';
8 import { ThemeProvider } from '@/components/theme/ThemeProvider';
9 import { LogoutButton } from '@/components/auth/logoutButton';
10 import { getCurrentUser } from '@/lib/auth';
11 import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
12 import { headers } from 'next/headers';
13 import { ShopShell } from '@/components/shop/shop-shell';
14
15 export const dynamic = 'force-dynamic';
16
17 export default async function LocaleLayout({
18 children,
19 params,
20 }: Readonly<{
21 children: React.ReactNode;
22 params: Promise<{ locale: string }>;
23 }>) {
24 const { locale } = await params;
25
26 if (!locales.includes(locale as any)) {
27 notFound();
28 }
29
30 const messages = await getMessages({ locale });
31 const user = await getCurrentUser();
32 const scope = (await headers()).get('x-app-scope') ?? 'site';
33 const showAdminNavLink = process.env.NEXT_PUBLIC_ENABLE_ADMIN === 'true';
34
35 return (
36 <NextIntlClientProvider messages={messages}>
37 <ThemeProvider
38 attribute="class"
39 defaultTheme="system"
40 enableSystem
41 disableTransitionOnChange
42 >
43 {scope === 'shop' ? (
44 <ShopShell showAdminLink={showAdminNavLink}>
45 <main className="mx-auto px-6 min-h-[80vh]">{children}</main>
46 </ShopShell>
47 ) : (
48 <>
49 <header className="bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 shadow-sm sticky top-0 z-50 transition-colors">
50 <div className="max-w-5xl mx-auto flex items-center justify-between px-6 py-4">
51 <Link href="/" className="flex items-center gap-2">
52 <span className="text-xl font-bold text-blue-600 dark:text-blue-500">
53 DevLovers
54 </span>
55 </Link>
56
57 <nav className="flex items-center gap-6 text-gray-700 dark:text-gray-300 font-medium">
58 <Link
59 href="/q&a"
60 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
61 >
62 Q&A
63 </Link>
64 <Link
65 href="/quiz/react-fundamentals"
66 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
67 >
68 Quiz
69 </Link>
70 <Link
71 href="/leaderboard"
72 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
73 >
74 Leaderboard
75 </Link>
76 <Link
77 href="/blog"
78 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
79 >
80 Blog
81 </Link>
82 <Link
83 href="/about"
84 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
85 >
86 About
87 </Link>
88 <Link
89 href="/contacts"
90 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
91 >
92 Contacts
93 </Link>
94 <Link
95 href="/shop"
96 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
97 >
98 Shop
99 </Link>
100
101 {!user ? (
102 <Link
103 href="/login"
104 className="hover:text-blue-600 dark:hover:text-blue-400 transition"
105 >
106 Log in
107 </Link>
108 ) : (
109 <LogoutButton />
110 )}
111
112 <LanguageSwitcher />
113 </nav>
114 </div>
115 </header>
116
117 <main className="mx-auto px-6 min-h-[80vh]">{children}</main>
118 </>
119 )}
120 <Footer />
121
122 <Toaster position="top-right" richColors expand />
123 </ThemeProvider>
124 </NextIntlClientProvider>
125 );
126 }
Script executed:
# Check for middleware configuration
git ls-files | grep -E "(middleware|next\.config)"Repository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
frontend/middleware.ts
frontend/next.config.ts
Next.js 15 i18n lang attribute best practices multiple locales
Here are concise best-practice recommendations for handling the HTML lang attribute and multiple locales in Next.js (v15 / App + Pages routing patterns):
-
Configure Next.js i18n in next.config.js (locales + defaultLocale). Next will manage locale routing and add the html lang automatically for Pages router; for App router you can set in your root layout. [1][2]
-
Use canonical UTS locale identifiers (e.g., en, en-US, pt-BR). Include region variants only when you actually support them (they act as fallbacks). [1]
-
Static generation: generate locale-specific routes (generateStaticParams or getStaticPaths) so each locale renders with the correct lang attribute at build time. [2]
-
SEO: add hreflang links for alternate locale/region variants and canonical tags (Next.js does not auto-create hreflang for page variants). [1]
-
Locale detection & persistence: use Next’s localeDetection config and the NEXT_LOCALE cookie to control redirects and persist user choice; set localeDetection:false if you want full manual control. [1][3]
-
Domain routing: if using domain-per-locale, declare domain mappings in i18n config so the correct defaultLocale and html lang are applied per domain. [1]
-
Right-to-left (RTL) languages: set dir on (dir="rtl") when appropriate (App router root layout or custom Document) alongside lang. [2][7]
-
Accessibility & correctness: ensure the lang value exactly matches the content language on the page (don’t set a single site-wide lang if pages contain different languages). [1][2]
References
- Next.js i18n (Pages / guides). [1]
- Next.js App Router internationalization (app/[lang] layouts, generateStaticParams, setting ). [2]
- NEXT_LOCALE cookie & locale detection notes. [3]
Script executed:
# Check if [locale] layout uses metadata export
grep -n "export const metadata\|export async function generateMetadata" frontend/app/\[locale\]/layout.tsxRepository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 1
Script executed:
# Check i18n config to understand locale setup
cat -n frontend/i18n/config.ts 2>/dev/null || find frontend -name "config.ts" -o -name "config.js" | grep i18n | head -5Repository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
1 export const locales = ['uk', 'en', 'pl'] as const;
2 export type Locale = (typeof locales)[number];
3
4 export const defaultLocale: Locale = 'uk';
5
6 export const localeNames: Record<Locale, string> = {
7 uk: 'Українська',
8 en: 'English',
9 pl: 'Polski',
10 };
Script executed:
# Check middleware to see if it handles locale routing
head -50 frontend/middleware.tsRepository: DevLoversTeam/devlovers.net
Repository: DevLoversTeam/devlovers.net
Exit code: 0
stdout:
import { NextRequest, NextResponse } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
const AUTH_COOKIE_NAME = 'auth_session';
const _AUTH_SECRET = process.env.AUTH_SECRET;
if (!_AUTH_SECRET) {
throw new Error('AUTH_SECRET is not defined');
}
function isAuthenticated(req: NextRequest): boolean {
return Boolean(req.cookies.get(AUTH_COOKIE_NAME)?.value);
}
const intlMiddleware = createIntlMiddleware(routing);
function authMiddleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const authenticated = isAuthenticated(req);
const pathnameWithoutLocale = pathname.replace(/^\/(uk|en|pl)/, '') || '/';
if (
(pathnameWithoutLocale === '/login' || pathnameWithoutLocale === '/signup') && authenticated
) {
const locale = pathname.split('/')[1] || 'uk';
return NextResponse.redirect(new URL(`/${locale}/`, req.url));
}
if (
pathnameWithoutLocale.startsWith('/leaderboard') ||
pathnameWithoutLocale.startsWith('/quiz') ||
pathnameWithoutLocale.startsWith('/dashboard')
) {
if (!authenticated) {
const locale = pathname.split('/')[1];
return NextResponse.redirect(new URL(`/${locale}/login`, req.url));
}
}
return null;
}
function getScopeFromPathname(pathname: string): "shop" | "site" {
const pathnameWithoutLocale = pathname.replace(/^\/(uk|en|pl)(?=\/|$)/, "") || "/"
return pathnameWithoutLocale.startsWith("/shop") ? "shop" : "site"
}
Hard-coded lang="uk" prevents correct language declaration for non-Ukrainian locales.
The lang attribute is hard-coded to "uk" in the root layout, but the application supports multiple locales (uk, en, pl). This causes all routes—including /en/* and /pl/*—to declare Ukrainian as the page language, creating accessibility issues (screen readers won't apply correct pronunciation) and SEO problems (search engines misidentify content language).
Since the root layout cannot access the [locale] segment parameter in Next.js 15's App Router hierarchy, the lang attribute must be handled in the locale-specific layout. Remove lang from the root layout and set it dynamically in frontend/app/[locale]/layout.tsx via metadata or a lang provider component that passes the correct language to the HTML element.
🤖 Prompt for AI Agents
In frontend/app/layout.tsx around line 20, remove the hard-coded lang="uk" from
the root <html> element so the root layout no longer declares a fixed language;
then add the language declaration in the locale-specific layout at
frontend/app/[locale]/layout.tsx by setting the HTML lang dynamically (either
via Next.js metadata export or by rendering the <html lang={locale}> in that
file or using a small provider component that injects the correct lang into the
HTML element) so each locale route (uk, en, pl) sets the appropriate lang
attribute.
| (pathnameWithoutLocale === '/login' || pathnameWithoutLocale === '/signup') && authenticated | ||
| ) { | ||
| return NextResponse.redirect(new URL(pathname.split('/').slice(0, 2).join('/') || '/', req.url)); | ||
| const locale = pathname.split('/')[1] || 'uk'; | ||
| return NextResponse.redirect(new URL(`/${locale}/`, req.url)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify pathname structure in authentication routes
# Find all redirects and route definitions related to authentication
echo "Authentication route patterns:"
rg -n "login|signup" frontend/app --type ts 2>/dev/null || echo "No matches in frontend/app"
# Check auth-related paths in middleware
echo -e "\nAuthentication-related middleware logic:"
rg -n "login|signup" frontend/middleware.ts -A 3 -B 3
# Look for other places where locale is extracted from pathname
echo -e "\nOther locale extraction patterns:"
rg -n "pathname\.split" frontend --type ts
# Also check the exact middleware file structure
echo -e "\nFull middleware.ts context around lines 26-30:"
cat -n frontend/middleware.ts | sed -n '20,35p'Repository: DevLoversTeam/devlovers.net
Length of output: 2671
Address inconsistent locale fallback in middleware redirects.
The locale extraction pattern pathname.split('/')[1] is used in two places with different fallback handling:
- Line 28: Has fallback
|| 'uk'when redirecting authenticated users away from login/signup—this is appropriate. - Line 38: Same extraction without fallback when redirecting unauthenticated users—if a request somehow lacks a valid locale prefix, this would extract an invalid value like
'login'and create malformed redirects like/{login}/login.
Given the Next.js [locale] dynamic segment routing, valid requests should always include a locale. However, add the fallback on line 38 to match line 28 for consistency and resilience: const locale = pathname.split('/')[1] || 'uk';
🤖 Prompt for AI Agents
In frontend/middleware.ts around lines 26-30 (and also update the similar logic
near line ~38), the locale extraction uses pathname.split('/')[1] without a safe
fallback in one place causing malformed redirects; update the second occurrence
to use const locale = pathname.split('/')[1] || 'uk'; so both redirect branches
consistently fall back to 'uk' when no valid locale segment is present.
fix(i18n): preserve locale across navigation and language switching
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.