Skip to content

Commit 2b7a7ad

Browse files
authored
Merge pull request #113 from strapi/feature/add-more-fr-localisation
fix: fully translatable LaunchPad
2 parents df474c9 + 4e48ebd commit 2b7a7ad

30 files changed

Lines changed: 882 additions & 417 deletions

File tree

README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,6 @@ Visit http://localhost:1337/admin to create your first Strapi user, and http://l
9393

9494
[Docs](https://docs.strapi.io)[Discord](https://discord.strapi.io)[YouTube](https://www.youtube.com/c/Strapi/featured)[Strapi Design System](https://design-system.strapi.io/)[Marketplace](https://market.strapi.io/)[Cloud Free Trial](https://cloud.strapi.io)
9595

96-
## Todo
97-
98-
- [ ] Implement the official Strapi SEO plugin
99-
- [ ] Create localized content for the pricing plans and products
100-
- [ ] Populate creator fields when it'll work on Strapi 5 (article authors information are missing)
101-
10296
## Customization
10397

10498
- The Strapi application contains a custom population middlewares in every api route.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default async function SingleArticlePage({
1616
$eq: slug,
1717
},
1818
},
19+
locale,
1920
});
2021

2122
if (!article) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default async function Blog({ params }: LocaleParamsProps) {
6767
key={firstArticle.title}
6868
/>
6969

70-
<BlogPostRows articles={articles} />
70+
<BlogPostRows articles={articles} locale={locale} />
7171
</Container>
7272
</div>
7373
);

next/app/[locale]/(marketing)/products/[slug]/page.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import { Metadata } from 'next';
22
import { redirect } from 'next/navigation';
33

44
import { Container } from '@/components/container';
5+
import ClientSlugHandler from '../../ClientSlugHandler';
56
import { AmbientColor } from '@/components/decorations/ambient-color';
67
import DynamicZoneManager from '@/components/dynamic-zone/manager';
78
import { SingleProduct } from '@/components/products/single-product';
89
import { generateMetadataObject } from '@/lib/shared/metadata';
9-
import { fetchCollectionType } from '@/lib/strapi';
10+
import { fetchCollectionType, fetchSingleType } from '@/lib/strapi';
1011
import type { LocaleSlugParamsProps, Product } from '@/types/types';
1112

1213
export async function generateMetadata({
1314
params,
1415
}: LocaleSlugParamsProps): Promise<Metadata> {
15-
const { slug } = await params;
16+
const { slug, locale } = await params;
1617

1718
const [pageData] = await fetchCollectionType<Product[]>('products', {
1819
filters: { slug: { $eq: slug } },
20+
locale,
1921
});
2022

2123
const seo = pageData;
@@ -30,17 +32,34 @@ export default async function SingleProductPage({
3032

3133
const [pageData] = await fetchCollectionType<Product[]>('products', {
3234
filters: { slug: { $eq: slug } },
35+
locale,
3336
});
3437

38+
const globalData = await fetchSingleType('global', { locale });
39+
3540
if (!pageData) {
3641
redirect('/products');
3742
}
3843

44+
const localizedSlugs = pageData.localizations?.reduce(
45+
(acc: Record<string, string>, localization: any) => {
46+
acc[localization.locale] = localization.slug;
47+
return acc;
48+
},
49+
{ [locale]: slug }
50+
) || {};
51+
3952
return (
4053
<div className="relative overflow-hidden w-full">
54+
<ClientSlugHandler localizedSlugs={localizedSlugs} />
4155
<AmbientColor />
4256
<Container className="py-20 md:py-40">
43-
<SingleProduct product={pageData} />
57+
<SingleProduct
58+
product={pageData}
59+
locale={locale}
60+
addToCartText={globalData.add_to_cart}
61+
buyNowText={globalData.buy_now}
62+
/>
4463
{pageData?.dynamic_zone && (
4564
<DynamicZoneManager
4665
dynamicZone={pageData?.dynamic_zone}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default async function Products({ params }: LocaleParamsProps) {
2929

3030
// Fetch the product-page and products data
3131
const pageData = await fetchSingleType('product-page', { locale });
32-
const products = await fetchCollectionType<Product[]>('products');
32+
const products = await fetchCollectionType<Product[]>('products', { locale });
3333

3434
const localizedSlugs = pageData.localizations.reduce(
3535
(acc: Record<string, string>, localization: any) => {
@@ -56,8 +56,18 @@ export default async function Products({ params }: LocaleParamsProps) {
5656
<Subheading className="max-w-3xl mx-auto">
5757
{pageData.sub_heading}
5858
</Subheading>
59-
<Featured products={featured} locale={locale} />
60-
<ProductItems products={products} locale={locale} />
59+
<Featured
60+
products={featured}
61+
locale={locale}
62+
heading={pageData.featured_heading}
63+
sub_heading={pageData.featured_sub_heading}
64+
/>
65+
<ProductItems
66+
products={products}
67+
locale={locale}
68+
heading={pageData.popular_heading}
69+
sub_heading={pageData.popular_sub_heading}
70+
/>
6171
</Container>
6272
</div>
6373
);

next/components/blog-post-rows.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import React, { useEffect, useState } from 'react';
88
import { truncate } from '@/lib/utils';
99
import { Article } from '@/types/types';
1010

11-
export const BlogPostRows = ({ articles }: { articles: Article[] }) => {
11+
export const BlogPostRows = ({
12+
articles,
13+
locale,
14+
}: {
15+
articles: Article[];
16+
locale: string;
17+
}) => {
1218
const [search, setSearch] = useState('');
1319

1420
const searcher = new FuzzySearch(articles, ['title'], {
@@ -40,18 +46,28 @@ export const BlogPostRows = ({ articles }: { articles: Article[] }) => {
4046
<p className="text-neutral-400 text-center p-4">No results found</p>
4147
) : (
4248
results.map((article, index) => (
43-
<BlogPostRow article={article} key={article.slug + index} />
49+
<BlogPostRow
50+
article={article}
51+
key={article.slug + index}
52+
locale={locale}
53+
/>
4454
))
4555
)}
4656
</div>
4757
</div>
4858
);
4959
};
5060

51-
export const BlogPostRow = ({ article }: { article: Article }) => {
61+
export const BlogPostRow = ({
62+
article,
63+
locale,
64+
}: {
65+
article: Article;
66+
locale: string;
67+
}) => {
5268
return (
5369
<Link
54-
href={`blog/${article.slug}`}
70+
href={`/${locale}/blog/${article.slug}`}
5571
key={`${article.slug}`}
5672
className="flex md:flex-row flex-col items-start justify-between md:items-center group py-4"
5773
>

next/components/dynamic-zone/pricing.tsx

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,36 @@ type Plan = {
2727
number: string;
2828
featured?: boolean;
2929
CTA?: CTA | undefined;
30+
localizations?: Plan[];
31+
locale?: string;
32+
};
33+
34+
// Helper to ensure the plan object has the correct shape if needed
35+
const normalizePlan = (plan: any): Plan => {
36+
return plan as Plan;
37+
};
38+
39+
const translations = {
40+
en: {
41+
currency: '$',
42+
featured: 'Featured',
43+
},
44+
fr: {
45+
currency: '€',
46+
featured: 'En vedette',
47+
},
3048
};
3149

3250
export const Pricing = ({
3351
heading,
3452
sub_heading,
3553
plans,
54+
locale = 'en',
3655
}: {
3756
heading: string;
3857
sub_heading: string;
3958
plans: any[];
59+
locale?: string;
4060
}) => {
4161
const onClick = (plan: Plan) => {
4262
console.log('click', plan);
@@ -51,95 +71,123 @@ export const Pricing = ({
5171
<Subheading className="max-w-3xl mx-auto">{sub_heading}</Subheading>
5272
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 max-w-7xl mx-auto gap-4 py-20 lg:items-start">
5373
{plans.map((plan) => (
54-
<Card onClick={() => onClick(plan)} key={plan.name} plan={plan} />
74+
<Card
75+
onClick={onClick}
76+
key={plan.name}
77+
plan={plan}
78+
locale={locale}
79+
/>
5580
))}
5681
</div>
5782
</Container>
5883
</div>
5984
);
6085
};
6186

62-
const Card = ({ plan, onClick }: { plan: Plan; onClick: () => void }) => {
87+
const Card = ({
88+
plan,
89+
onClick,
90+
locale,
91+
}: {
92+
plan: Plan;
93+
onClick: (plan: Plan) => void;
94+
locale: string;
95+
}) => {
96+
const t = translations[locale as keyof typeof translations] || translations.en;
97+
98+
// Try to find the plan content that matches the current locale
99+
// This handles cases where the page links to the English plan but a French version exists
100+
let displayPlan = plan;
101+
if (plan.localizations && plan.localizations.length > 0) {
102+
const localizedPlan = plan.localizations.find((p) => p.locale === locale);
103+
if (localizedPlan) {
104+
displayPlan = normalizePlan(localizedPlan); // helper to ensure shape matches if needed, but assuming same shape
105+
}
106+
}
107+
108+
// Ensure perks are also localized if the plan didn't switch or if structure is different
109+
// Assuming localizedPlan has its own perks.
110+
63111
return (
64112
<div
65113
className={cn(
66114
'p-4 md:p-4 rounded-3xl bg-neutral-900 border-2 border-neutral-800',
67-
plan.featured && 'border-neutral-50 bg-neutral-100'
115+
displayPlan.featured && 'border-neutral-50 bg-neutral-100'
68116
)}
69117
>
70118
<div
71119
className={cn(
72120
'p-4 bg-neutral-800 rounded-2xl shadow-[0px_-1px_0px_0px_var(--neutral-700)]',
73-
plan.featured && 'bg-white shadow-aceternity'
121+
displayPlan.featured && 'bg-white shadow-aceternity'
74122
)}
75123
>
76124
<div className="flex justify-between items-center">
77-
<p className={cn('font-medium', plan.featured && 'text-black')}>
78-
{plan.name}
125+
<p className={cn('font-medium', displayPlan.featured && 'text-black')}>
126+
{displayPlan.name}
79127
</p>
80-
{plan.featured && (
128+
{displayPlan.featured && (
81129
<div
82130
className={cn(
83131
'font-medium text-xs px-3 py-1 rounded-full relative bg-neutral-900'
84132
)}
85133
>
86134
<div className="absolute inset-x-0 bottom-0 w-3/4 mx-auto h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent"></div>
87-
Featured
135+
{t.featured}
88136
</div>
89137
)}
90138
</div>
91-
<div className="mt-8">
92-
{plan.price && (
139+
<div className="mt-8 flex items-baseline">
140+
{displayPlan.price && (
93141
<span
94142
className={cn(
95143
'text-lg font-bold text-neutral-500',
96-
plan.featured && 'text-neutral-700'
144+
displayPlan.featured && 'text-neutral-700'
97145
)}
98146
>
99-
$
147+
{t.currency}
100148
</span>
101149
)}
102150
<span
103-
className={cn('text-4xl font-bold', plan.featured && 'text-black')}
151+
className={cn('text-4xl font-bold', displayPlan.featured && 'text-black')}
104152
>
105-
{plan.price || plan?.CTA?.text}
153+
{displayPlan.price || displayPlan?.CTA?.text}
106154
</span>
107-
{plan.price && (
155+
{displayPlan.price && (
108156
<span
109157
className={cn(
110158
'text-lg font-normal text-neutral-500 ml-2',
111-
plan.featured && 'text-neutral-700'
159+
displayPlan.featured && 'text-neutral-700'
112160
)}
113161
>
114-
/ launch
162+
launch
115163
</span>
116164
)}
117165
</div>
118166
<Button
119167
variant="outline"
120168
className={cn(
121169
'w-full mt-10 mb-4',
122-
plan.featured &&
123-
'bg-black text-white hover:bg-black/80 hover:text-white'
170+
displayPlan.featured &&
171+
'bg-black text-white hover:bg-black/80 hover:text-white'
124172
)}
125-
onClick={onClick}
173+
onClick={() => onClick(displayPlan)}
126174
>
127-
{plan?.CTA?.text}
175+
{displayPlan?.CTA?.text}
128176
</Button>
129177
</div>
130178
<div className="mt-1 p-4">
131-
{plan.perks.map((feature, idx) => (
132-
<Step featured={plan.featured} key={idx}>
179+
{displayPlan.perks.map((feature, idx) => (
180+
<Step featured={displayPlan.featured} key={idx}>
133181
{feature.text}
134182
</Step>
135183
))}
136184
</div>
137-
{plan.additional_perks && plan.additional_perks.length > 0 && (
138-
<Divider featured={plan.featured} />
185+
{displayPlan.additional_perks && displayPlan.additional_perks.length > 0 && (
186+
<Divider featured={displayPlan.featured} />
139187
)}
140188
<div className="p-4">
141-
{plan.additional_perks?.map((feature, idx) => (
142-
<Step featured={plan.featured} additional key={idx}>
189+
{displayPlan.additional_perks?.map((feature, idx) => (
190+
<Step featured={displayPlan.featured} additional key={idx}>
143191
{feature.text}
144192
</Step>
145193
))}
@@ -148,6 +196,7 @@ const Card = ({ plan, onClick }: { plan: Plan; onClick: () => void }) => {
148196
);
149197
};
150198

199+
151200
const Step = ({
152201
children,
153202
additional,

next/components/locale-switcher.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,20 @@ export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) {
3737

3838
return (
3939
<div className="flex gap-2 p-1 rounded-md">
40-
{!pathname.includes('/products/') &&
41-
Object.keys(localizedSlugs).map((locale) => (
42-
<Link key={locale} href={generateLocalizedPath(locale)}>
43-
<div
44-
className={cn(
45-
'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',
46-
locale === currentLocale
47-
? 'bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]'
48-
: ''
49-
)}
50-
>
51-
{locale}
52-
</div>
53-
</Link>
54-
))}
40+
{Object.keys(localizedSlugs).map((locale) => (
41+
<Link key={locale} href={generateLocalizedPath(locale)}>
42+
<div
43+
className={cn(
44+
'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',
45+
locale === currentLocale
46+
? 'bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]'
47+
: ''
48+
)}
49+
>
50+
{locale}
51+
</div>
52+
</Link>
53+
))}
5554
</div>
5655
);
5756
}

0 commit comments

Comments
 (0)