Skip to content

Commit 5b24309

Browse files
committed
update: change preferredLocale in local storage follow switch locale button
1 parent eaa1356 commit 5b24309

File tree

7 files changed

+187
-55
lines changed

7 files changed

+187
-55
lines changed

.next/trace

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"name":"next-dev","duration":336397,"timestamp":257582122435,"id":1,"tags":{},"startTime":1749254546436,"traceId":"a69c61c90c63e8b2"}]

website/src/app/[locale]/blog/[slug]/page.tsx

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { Button } from "@/components/ui/button";
55
import Link from "next/link";
66
import MarkdownContent from "@/components/blog/markdown-content";
77
import { getContentAsBlogPosts, findAvailableTranslations } from "@/lib/content-mapper";
8-
import { Globe, ChevronDown, ChevronUp, Code, CalendarClock } from "lucide-react";
8+
import { Code, CalendarClock } from "lucide-react";
99
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
1010
import 'highlight.js/styles/github-dark.css';
1111
import CodeSnippet from "@/components/blog/code-snippet";
1212
import { generateDefaultMetadata } from '@/lib/metadata';
13+
import LanguageSwitcher from "@/components/blog/language-switcher";
1314

1415
type Params = Promise<{
1516
locale: Locale;
@@ -131,25 +132,11 @@ export default async function BlogPostPage({ params }: { params: Params }) {
131132
</Link>
132133

133134
{availableTranslations.length > 1 && (
134-
<div className="flex items-center gap-2">
135-
<Globe size={16} className="text-muted-foreground" />
136-
<span className="text-sm text-muted-foreground">{t.blog.translatedContent}:</span>
137-
<div className="flex gap-2">
138-
{availableTranslations.map(translation => (
139-
<Link
140-
key={translation.locale}
141-
href={`/${translation.locale}/blog/${translation.slug}`}
142-
className={`text-sm px-2 py-1 rounded cursor-pointer ${
143-
translation.locale === locale
144-
? 'bg-primary text-primary-foreground'
145-
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
146-
}`}
147-
>
148-
{t.languages[translation.locale as keyof typeof t.languages] || translation.locale.toUpperCase()}
149-
</Link>
150-
))}
151-
</div>
152-
</div>
135+
<LanguageSwitcher
136+
currentLocale={locale}
137+
translations={translations}
138+
availableTranslations={availableTranslations}
139+
/>
153140
)}
154141
</div>
155142

website/src/app/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { defaultLocale } from '@/lib/i18n/settings';
6-
import Image from "next/image";
6+
import { setCookie } from '@/lib/cookies';
77

88
export default function Home() {
99
const router = useRouter();
@@ -14,6 +14,8 @@ export default function Home() {
1414

1515
if (savedLocale) {
1616
// If there is, use the saved locale
17+
// Also ensure the cookie is set
18+
setCookie('preferredLocale', savedLocale, 365);
1719
router.push(`/${savedLocale}`);
1820
} else {
1921
// If not, try to determine the locale from the browser
@@ -23,6 +25,9 @@ export default function Home() {
2325
// Save to localStorage for future use
2426
localStorage.setItem('preferredLocale', locale);
2527

28+
// Also set a cookie for the middleware
29+
setCookie('preferredLocale', locale, 365);
30+
2631
// Redirect to the page with the determined locale
2732
router.push(`/${locale}`);
2833
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
import { Locale } from '@/lib/i18n/settings';
5+
import { useEffect } from 'react';
6+
import { setCookie } from '@/lib/cookies';
7+
import { Globe } from 'lucide-react';
8+
9+
interface LanguageSwitcherProps {
10+
currentLocale: Locale;
11+
translations: any;
12+
availableTranslations: Array<{
13+
locale: string;
14+
slug: string;
15+
}>;
16+
}
17+
18+
export default function LanguageSwitcher({
19+
currentLocale,
20+
translations,
21+
availableTranslations,
22+
}: LanguageSwitcherProps) {
23+
const t = translations.common;
24+
25+
// Update preferredLocale in localStorage when the component mounts
26+
useEffect(() => {
27+
// Update localStorage and cookie with the current locale
28+
if (typeof window !== 'undefined') {
29+
localStorage.setItem('preferredLocale', currentLocale);
30+
setCookie('preferredLocale', currentLocale, 365); // Valid for 1 year
31+
}
32+
}, [currentLocale]);
33+
34+
// Handle language change (for analytics or other side effects)
35+
const handleLanguageClick = (newLocale: string) => {
36+
// Update localStorage and cookie
37+
if (typeof window !== 'undefined') {
38+
localStorage.setItem('preferredLocale', newLocale);
39+
setCookie('preferredLocale', newLocale, 365); // Valid for 1 year
40+
}
41+
};
42+
43+
if (availableTranslations.length <= 1) {
44+
return null;
45+
}
46+
47+
return (
48+
<div className="flex items-center gap-2">
49+
<Globe size={16} className="text-muted-foreground" />
50+
<span className="text-sm text-muted-foreground">{t.blog.translatedContent}:</span>
51+
<div className="flex gap-2">
52+
{availableTranslations.map(translation => (
53+
<Link
54+
key={translation.locale}
55+
href={`/${translation.locale}/blog/${translation.slug}`}
56+
onClick={() => handleLanguageClick(translation.locale)}
57+
className={`text-sm px-2 py-1 rounded cursor-pointer ${
58+
translation.locale === currentLocale
59+
? 'bg-primary text-primary-foreground'
60+
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
61+
}`}
62+
>
63+
{t.languages[translation.locale as keyof typeof t.languages] || translation.locale.toUpperCase()}
64+
</Link>
65+
))}
66+
</div>
67+
</div>
68+
);
69+
}

website/src/components/layout/header.tsx

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/s
99
import { Menu } from "lucide-react";
1010
import { siteConfig } from "@/config/site";
1111
import { Locale } from "@/lib/i18n/settings";
12+
import { useRouter } from "next/navigation";
13+
import { setCookie } from "@/lib/cookies";
1214

1315
interface HeaderProps {
1416
translations: any;
@@ -18,11 +20,24 @@ interface HeaderProps {
1820
export default function Header({ translations, locale }: HeaderProps) {
1921
const t = translations.common;
2022
const { theme, setTheme } = useTheme();
23+
const router = useRouter();
2124

2225
const toggleTheme = () => {
2326
setTheme(theme === "dark" ? "light" : "dark");
2427
};
2528

29+
const handleLanguageChange = (newLocale: Locale) => {
30+
// Save the preferred locale to localStorage
31+
if (typeof window !== "undefined") {
32+
localStorage.setItem('preferredLocale', newLocale);
33+
34+
// Set a cookie for the middleware (which can't access localStorage)
35+
setCookie('preferredLocale', newLocale, 365); // Valid for 1 year
36+
}
37+
38+
router.push(`/${newLocale}`);
39+
};
40+
2641
return (
2742
<header className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
2843
<div className="w-full flex h-16 md:h-20 items-center px-4 md:px-6 lg:px-8">
@@ -52,24 +67,22 @@ export default function Header({ translations, locale }: HeaderProps) {
5267
{theme === "dark" ? <SunIcon size={18} /> : <MoonIcon size={18} />}
5368
</Button>
5469
<div className="flex items-center gap-1 ml-2 sm:ml-4 border rounded-full px-1 py-0.5">
55-
<Link href="/en" locale="en" className="cursor-pointer">
56-
<Button
57-
variant="ghost"
58-
size="sm"
59-
className={`rounded-full px-2 sm:px-3 text-xs sm:text-sm h-7 sm:h-8 ${locale === "en" ? "bg-primary text-primary-foreground" : ""}`}
60-
>
61-
EN
62-
</Button>
63-
</Link>
64-
<Link href="/vi" locale="vi" className="cursor-pointer">
65-
<Button
66-
variant="ghost"
67-
size="sm"
68-
className={`rounded-full px-2 sm:px-3 text-xs sm:text-sm h-7 sm:h-8 ${locale === "vi" ? "bg-primary text-primary-foreground" : ""}`}
69-
>
70-
VI
71-
</Button>
72-
</Link>
70+
<Button
71+
variant="ghost"
72+
size="sm"
73+
className={`rounded-full px-2 sm:px-3 text-xs sm:text-sm h-7 sm:h-8 ${locale === "en" ? "bg-primary text-primary-foreground" : ""}`}
74+
onClick={() => handleLanguageChange("en")}
75+
>
76+
EN
77+
</Button>
78+
<Button
79+
variant="ghost"
80+
size="sm"
81+
className={`rounded-full px-2 sm:px-3 text-xs sm:text-sm h-7 sm:h-8 ${locale === "vi" ? "bg-primary text-primary-foreground" : ""}`}
82+
onClick={() => handleLanguageChange("vi")}
83+
>
84+
VI
85+
</Button>
7386
</div>
7487
</div>
7588
<Sheet>
@@ -104,18 +117,20 @@ export default function Header({ translations, locale }: HeaderProps) {
104117
<div className="space-y-3">
105118
<h3 className="text-sm uppercase text-muted-foreground font-medium">{t.languages?.title || 'Languages'}</h3>
106119
<div className="flex flex-col space-y-2">
107-
<Link href="/en" locale="en" className="cursor-pointer">
108-
<div className={`flex items-center px-3 py-2 rounded-lg ${locale === "en" ? "bg-primary/10 text-primary" : ""}`}>
109-
<span className="text-base">{t.languages?.english || 'English'}</span>
110-
{locale === "en" && <span className="ml-auto text-sm bg-primary text-primary-foreground px-2 py-0.5 rounded-full">Active</span>}
111-
</div>
112-
</Link>
113-
<Link href="/vi" locale="vi" className="cursor-pointer">
114-
<div className={`flex items-center px-3 py-2 rounded-lg ${locale === "vi" ? "bg-primary/10 text-primary" : ""}`}>
115-
<span className="text-base">{t.languages?.vietnamese || 'Tiếng Việt'}</span>
116-
{locale === "vi" && <span className="ml-auto text-sm bg-primary text-primary-foreground px-2 py-0.5 rounded-full">Active</span>}
117-
</div>
118-
</Link>
120+
<div
121+
className={`flex items-center px-3 py-2 rounded-lg cursor-pointer ${locale === "en" ? "bg-primary/10 text-primary" : ""}`}
122+
onClick={() => handleLanguageChange("en")}
123+
>
124+
<span className="text-base">{t.languages?.english || 'English'}</span>
125+
{locale === "en" && <span className="ml-auto text-sm bg-primary text-primary-foreground px-2 py-0.5 rounded-full">Active</span>}
126+
</div>
127+
<div
128+
className={`flex items-center px-3 py-2 rounded-lg cursor-pointer ${locale === "vi" ? "bg-primary/10 text-primary" : ""}`}
129+
onClick={() => handleLanguageChange("vi")}
130+
>
131+
<span className="text-base">{t.languages?.vietnamese || 'Tiếng Việt'}</span>
132+
{locale === "vi" && <span className="ml-auto text-sm bg-primary text-primary-foreground px-2 py-0.5 rounded-full">Active</span>}
133+
</div>
119134
</div>
120135
</div>
121136
<div className="space-y-3">

website/src/lib/cookies.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Set a cookie with the given name, value, and expiry days
3+
*/
4+
export function setCookie(name: string, value: string, days: number) {
5+
if (typeof document === 'undefined') return;
6+
7+
let expires = '';
8+
if (days) {
9+
const date = new Date();
10+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
11+
expires = `; expires=${date.toUTCString()}`;
12+
}
13+
14+
document.cookie = `${name}=${value || ''}${expires}; path=/; SameSite=Lax;`;
15+
}
16+
17+
/**
18+
* Get a cookie value by name
19+
*/
20+
export function getCookie(name: string): string | null {
21+
if (typeof document === 'undefined') return null;
22+
23+
const nameEQ = `${name}=`;
24+
const ca = document.cookie.split(';');
25+
26+
for (let i = 0; i < ca.length; i++) {
27+
let c = ca[i];
28+
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
29+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
30+
}
31+
32+
return null;
33+
}
34+
35+
/**
36+
* Delete a cookie by name
37+
*/
38+
export function deleteCookie(name: string) {
39+
if (typeof document === 'undefined') return;
40+
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=Lax;`;
41+
}

website/src/middleware.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { defaultLocale, locales } from '@/lib/i18n/settings';
44
// List of countries using Vietnamese as the main language
55
const VIETNAMESE_COUNTRIES = ['VN', 'VNM'];
66

7+
// Cookie name for storing preferred locale
8+
const PREFERRED_LOCALE_COOKIE = 'preferredLocale';
9+
710
// Middleware executed before each request
811
export function middleware(request: NextRequest) {
912
// Get the pathname from the URL
@@ -29,11 +32,17 @@ export function middleware(request: NextRequest) {
2932
return NextResponse.next();
3033
}
3134

35+
// Get preferred locale from cookies
36+
const preferredLocale = request.cookies.get(PREFERRED_LOCALE_COOKIE)?.value;
37+
3238
// If it's the root path, redirect to the appropriate locale
3339
if (pathname === '/') {
34-
// Get country code from x-vercel-ip-country header (if running on Vercel)
35-
// or cf-ipcountry header (if running on Cloudflare)
36-
// or x-country header (if you configure a different CDN)
40+
// If user has a preferred locale saved, use that
41+
if (preferredLocale && locales.includes(preferredLocale as any)) {
42+
return NextResponse.redirect(new URL(`/${preferredLocale}`, request.url));
43+
}
44+
45+
// Otherwise, try to determine locale from country code
3746
const countryCode =
3847
request.headers.get('x-vercel-ip-country') ||
3948
request.headers.get('cf-ipcountry') ||
@@ -48,7 +57,12 @@ export function middleware(request: NextRequest) {
4857
return NextResponse.redirect(new URL(`/${locale}`, request.url));
4958
}
5059

51-
// If pathname doesn't have locale, add default locale
60+
// For other paths without locale, check preferred locale first
61+
if (preferredLocale && locales.includes(preferredLocale as any)) {
62+
return NextResponse.redirect(new URL(`/${preferredLocale}${pathname}`, request.url));
63+
}
64+
65+
// If no preferred locale, add default locale
5266
const newUrl = new URL(`/${defaultLocale}${pathname}`, request.url);
5367

5468
// Redirect to URL with default locale

0 commit comments

Comments
 (0)