Skip to content

Commit 33cc961

Browse files
authored
Merge pull request #24 from strapi/feat/locale-switcher-logic
chore: create a locale switcher logic
2 parents 13fcb28 + 1560122 commit 33cc961

File tree

13 files changed

+346
-214
lines changed

13 files changed

+346
-214
lines changed

Diff for: next/app/[locale]/(marketing)/ClientSlugHandler.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import { useSlugContext } from "@/app/context/SlugContext";
5+
6+
export default function ClientSlugHandler({
7+
localizedSlugs,
8+
}: {
9+
localizedSlugs: Record<string, string>;
10+
}) {
11+
const { dispatch } = useSlugContext();
12+
13+
useEffect(() => {
14+
if (localizedSlugs) {
15+
dispatch({ type: "SET_SLUGS", payload: localizedSlugs });
16+
}
17+
}, [localizedSlugs, dispatch]);
18+
19+
return null; // This component only handles the state and doesn't render anything.
20+
}

Diff for: next/app/[locale]/(marketing)/[slug]/page.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Metadata } from 'next';
33
import PageContent from '@/lib/shared/PageContent';
44
import fetchContentType from '@/lib/strapi/fetchContentType';
55
import { generateMetadataObject } from '@/lib/shared/metadata';
6+
import ClientSlugHandler from '../ClientSlugHandler';
67

78
export async function generateMetadata({
89
params,
@@ -27,7 +28,19 @@ export default async function Page({ params }: { params: { locale: string, slug:
2728
true
2829
);
2930

31+
const localizedSlugs = pageData.localizations?.reduce(
32+
(acc: Record<string, string>, localization: any) => {
33+
acc[localization.locale] = localization.slug;
34+
return acc;
35+
},
36+
{ [params.locale]: params.slug }
37+
);
38+
3039
return (
31-
<PageContent pageData={pageData} />
40+
<>
41+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
42+
<PageContent pageData={pageData} />
43+
</>
44+
3245
);
3346
}

Diff for: next/app/[locale]/(marketing)/blog/[slug]/page.tsx

+21-16
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1-
import { Metadata } from 'next';
1+
import React from "react";
22

33
import { BlogLayout } from "@/components/blog-layout";
44
import fetchContentType from "@/lib/strapi/fetchContentType";
5-
import { BlocksRenderer } from '@strapi/blocks-react-renderer';
5+
import { BlocksRenderer } from "@strapi/blocks-react-renderer";
66

7-
import { generateMetadataObject } from '@/lib/shared/metadata';
7+
import ClientSlugHandler from "../../ClientSlugHandler";
88

9-
export async function generateMetadata({
9+
export default async function SingleArticlePage({
1010
params,
1111
}: {
12-
params: { locale: string, slug: string };
13-
}): Promise<Metadata> {
14-
const pageData = await fetchContentType("articles", `filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}&populate=seo.metaImage`, true)
15-
16-
const seo = pageData?.seo;
17-
const metadata = generateMetadataObject(seo);
18-
return metadata;
19-
}
20-
21-
export default async function singleArticlePage({ params }: { params: { slug: string, locale: string } }) {
22-
const article = await fetchContentType("articles", `filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}`, true)
12+
params: { slug: string; locale: string };
13+
}) {
14+
const article = await fetchContentType(
15+
"articles",
16+
`filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}`,
17+
true
18+
);
2319

2420
if (!article) {
2521
return <div>Blog not found</div>;
2622
}
2723

24+
const localizedSlugs = article.localizations?.reduce(
25+
(acc: Record<string, string>, localization: any) => {
26+
acc[localization.locale] = localization.slug;
27+
return acc;
28+
},
29+
{ [params.locale]: params.slug }
30+
);
31+
2832
return (
2933
<BlogLayout article={article} locale={params.locale}>
34+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
3035
<BlocksRenderer content={article.content} />
3136
</BlogLayout>
3237
);
33-
}
38+
}

Diff for: next/app/[locale]/(marketing)/blog/page.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import fetchContentType from "@/lib/strapi/fetchContentType";
1111
import { Article } from "@/types/types";
1212
import { generateMetadataObject } from '@/lib/shared/metadata';
1313

14+
import ClientSlugHandler from "../ClientSlugHandler";
1415

1516
export async function generateMetadata({
1617
params,
@@ -27,13 +28,22 @@ export async function generateMetadata({
2728
export default async function Blog({
2829
params,
2930
}: {
30-
params: { locale: string };
31+
params: { locale: string, slug: string };
3132
}) {
3233
const blogPage = await fetchContentType('blog-page', `filters[locale]=${params.locale}`, true)
3334
const articles = await fetchContentType('articles', `filters[locale]=${params.locale}`)
3435

36+
const localizedSlugs = blogPage.localizations?.reduce(
37+
(acc: Record<string, string>, localization: any) => {
38+
acc[localization.locale] = "blog";
39+
return acc;
40+
},
41+
{ [params.locale]: "blog" }
42+
);
43+
3544
return (
3645
<div className="relative overflow-hidden py-20 md:py-0">
46+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
3747
<AmbientColor />
3848
<Container className="flex flex-col items-center justify-between pb-20">
3949
<div className="relative z-20 py-10 md:pt-40">

Diff for: next/app/[locale]/(marketing)/page.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Metadata } from 'next';
33
import PageContent from '@/lib/shared/PageContent';
44
import fetchContentType from '@/lib/strapi/fetchContentType';
55
import { generateMetadataObject } from '@/lib/shared/metadata';
6+
import ClientSlugHandler from './ClientSlugHandler';
67

78
export async function generateMetadata({
89
params,
@@ -27,5 +28,16 @@ export default async function HomePage({ params }: { params: { locale: string }
2728
true
2829
);
2930

30-
return <PageContent pageData={pageData} />;
31+
const localizedSlugs = pageData.localizations?.reduce(
32+
(acc: Record<string, string>, localization: any) => {
33+
acc[localization.locale] = "";
34+
return acc;
35+
},
36+
{ [params.locale]: "" }
37+
);
38+
39+
return <>
40+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
41+
<PageContent pageData={pageData} />
42+
</>;
3143
}

Diff for: next/app/[locale]/(marketing)/products/page.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { IconShoppingCartUp } from "@tabler/icons-react";
1111
import fetchContentType from "@/lib/strapi/fetchContentType";
1212
import { generateMetadataObject } from '@/lib/shared/metadata';
1313

14+
import ClientSlugHandler from '../ClientSlugHandler';
15+
1416
export async function generateMetadata({
1517
params,
1618
}: {
@@ -32,10 +34,18 @@ export default async function Products({
3234
const productPage = await fetchContentType('product-page', `filters[locale]=${params.locale}`, true);
3335
const products = await fetchContentType('products', ``);
3436

37+
const localizedSlugs = productPage.localizations?.reduce(
38+
(acc: Record<string, string>, localization: any) => {
39+
acc[localization.locale] = "products";
40+
return acc;
41+
},
42+
{ [params.locale]: "products" }
43+
);
3544
const featured = products?.data.filter((product: { featured: boolean }) => product.featured);
3645

3746
return (
3847
<div className="relative overflow-hidden w-full">
48+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
3949
<AmbientColor />
4050
<Container className="pt-40 pb-40">
4151
<FeatureIconContainer className="flex justify-center items-center overflow-hidden">

Diff for: next/app/context/SlugContext.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import React, { createContext, useContext, useReducer } from "react";
4+
5+
type State = {
6+
localizedSlugs: Record<string, string>;
7+
};
8+
9+
type Action = {
10+
type: "SET_SLUGS";
11+
payload: Record<string, string>;
12+
};
13+
14+
const SlugContext = createContext<{
15+
state: State;
16+
dispatch: React.Dispatch<Action>;
17+
} | null>(null);
18+
19+
const slugReducer = (state: State, action: Action): State => {
20+
switch (action.type) {
21+
case "SET_SLUGS":
22+
return { ...state, localizedSlugs: action.payload };
23+
default:
24+
return state;
25+
}
26+
};
27+
28+
export const SlugProvider = ({ children }: { children: React.ReactNode }) => {
29+
const [state, dispatch] = useReducer(slugReducer, { localizedSlugs: {} });
30+
31+
return (
32+
<SlugContext.Provider value={{ state, dispatch }}>
33+
{children}
34+
</SlugContext.Provider>
35+
);
36+
};
37+
38+
export const useSlugContext = () => {
39+
const context = useContext(SlugContext);
40+
if (!context) {
41+
throw new Error("useSlugContext must be used within a SlugProvider");
42+
}
43+
return context;
44+
};

Diff for: next/app/layout.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Locale, i18n } from '@/i18n.config'
33

44
import "./globals.css";
55

6+
import { SlugProvider } from "./context/SlugContext";
7+
68
export const viewport: Viewport = {
79
themeColor: [
810
{ media: "(prefers-color-scheme: light)", color: "#06b6d4" },
@@ -24,7 +26,9 @@ export default function RootLayout({
2426
return (
2527
<html lang={params.lang}>
2628
<body>
27-
{children}
29+
<SlugProvider>
30+
{children}
31+
</SlugProvider>
2832
</body>
2933
</html>
3034
);

Diff for: next/components/locale-switcher.tsx

+43-32
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,55 @@
1+
"use client";
2+
13
import React from "react";
4+
import Link from "next/link";
5+
import { usePathname } from "next/navigation";
6+
import { useSlugContext } from "@/app/context/SlugContext";
7+
import { cn } from "@/lib/utils";
28

9+
export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) {
10+
const { state } = useSlugContext();
11+
const { localizedSlugs } = state;
312

4-
import Link from 'next/link'
5-
import { usePathname } from 'next/navigation'
6-
import { i18n } from '@/i18n.config'
13+
const pathname = usePathname(); // Current path
14+
const segments = pathname.split("/"); // Split path into segments
715

8-
import { cn } from "@/lib/utils";
16+
// Generate localized path for each locale
17+
const generateLocalizedPath = (locale: string): string => {
18+
if (!pathname) return `/${locale}`; // Default to root path for the locale
19+
20+
// Handle homepage (e.g., "/en" -> "/fr")
21+
if (segments.length <= 2) {
22+
return `/${locale}`;
23+
}
924

10-
export function LocaleSwitcher() {
11-
const pathName = usePathname()
12-
const currentLocale = pathName.split('/')[1]
25+
// Handle dynamic paths (e.g., "/en/blog/[slug]")
26+
if (localizedSlugs[locale]) {
27+
segments[1] = locale; // Replace the locale
28+
segments[segments.length - 1] = localizedSlugs[locale]; // Replace slug if available
29+
return segments.join("/");
30+
}
1331

14-
const redirectedPathName = (locale: string) => {
15-
if (!pathName) return '/'
16-
const segments = pathName.split('/')
17-
segments[1] = locale
18-
return segments.join('/')
19-
}
32+
// Fallback to replace only the locale
33+
segments[1] = locale;
34+
return segments.join("/");
35+
};
2036

2137
return (
22-
<div className="flex gap-2 p-1 rounded-md">
23-
{i18n.locales.map((locale) => (
24-
<Link
25-
key={locale}
26-
href={redirectedPathName(locale)}
27-
>
28-
<React.Fragment >
29-
<div
30-
className={cn(
31-
"flex cursor-pointer items-center justify-center text-sm leading-[110%] w-8 py-1 rounded-md hover:bg-neutral-800 hover:text-white/80 text-white hover:shadow-[0px_1px_0px_0px_var(--neutral-600)_inset] transition duration-200",
32-
locale === currentLocale
33-
? "bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]"
34-
: ""
35-
)}
36-
>
37-
{locale}
38-
</div>
39-
</React.Fragment>
38+
<div className="flex gap-2 p-1 rounded-md">
39+
{!pathname.includes("/products/") && Object.keys(localizedSlugs).map((locale) => (
40+
<Link key={locale} href={generateLocalizedPath(locale)}>
41+
<div
42+
className={cn(
43+
"flex cursor-pointer items-center justify-center text-sm leading-[110%] w-8 py-1 rounded-md hover:bg-neutral-800 hover:text-white/80 text-white hover:shadow-[0px_1px_0px_0px_var(--neutral-600)_inset] transition duration-200",
44+
locale === currentLocale
45+
? "bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]"
46+
: ""
47+
)}
48+
>
49+
{locale}
50+
</div>
4051
</Link>
4152
))}
4253
</div>
4354
);
44-
}
55+
}

Diff for: next/components/navbar/desktop-navbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const DesktopNavbar = ({ leftNavbarItems, rightNavbarItems, logo, locale
7777
</div>
7878
</div>
7979
<div className="flex space-x-2 items-center">
80-
<LocaleSwitcher />
80+
<LocaleSwitcher currentLocale={locale} />
8181

8282
{rightNavbarItems.map((item, index) => (
8383
<Button key={item.text} variant={index === rightNavbarItems.length - 1 ? 'primary' : 'simple'} as={Link} href={`/${locale}${item.URL}`}>

Diff for: strapi/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
"typescript": "^5"
2020
},
2121
"dependencies": {
22-
"@strapi/plugin-cloud": "5.5.0",
22+
"@strapi/plugin-cloud": "5.5.1",
2323
"@strapi/plugin-seo": "^2.0.4",
24-
"@strapi/plugin-users-permissions": "5.5.0",
25-
"@strapi/strapi": "5.5.0",
26-
"better-sqlite3": "9.4.3",
24+
"@strapi/plugin-users-permissions": "5.5.1",
25+
"@strapi/strapi": "5.5.1",
26+
"better-sqlite3": "11.7.0",
2727
"patch-package": "^8.0.0",
2828
"pluralize": "^8.0.0",
2929
"react": "^18.0.0",

Diff for: strapi/src/middlewares/deepPopulate.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,18 @@ const getDeepPopulate = (uid: UID.Schema, opts: Options = {}) => {
8181

8282
export default (config, { strapi }: { strapi: Core.Strapi }) => {
8383
return async (ctx, next) => {
84-
if (ctx.request.url.startsWith('/api/') && ctx.request.method === 'GET' && !ctx.query.populate) {
84+
if (ctx.request.url.startsWith('/api/') && ctx.request.method === 'GET' && !ctx.query.populate && !ctx.request.url.includes('/api/users')) {
8585
strapi.log.info('Using custom Dynamic-Zone population Middleware...');
8686

8787
const contentType = extractPathSegment(ctx.request.url);
8888
const singular = pluralize.singular(contentType)
8989
const uid = `api::${singular}.${singular}`;
9090

91-
// @ts-ignores
92-
ctx.query.populate = getDeepPopulate(uid);
91+
ctx.query.populate = {
92+
// @ts-ignores
93+
...getDeepPopulate(uid),
94+
...(!ctx.request.url.includes("products") && { localizations: { populate: {} } })
95+
};
9396
}
9497
await next();
9598
};

0 commit comments

Comments
 (0)