feat: implement i18n for product and category, add 5 new locales
This commit is contained in:
1 parent
96d874233d
commit
2977df0a9d
8 files changed
+25
-25
No files matched your search
@@ -47,14 +47,14 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
|
|||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
||||||
<img
|
<img
|
||||||
src={cat.imageUrl || getCategoryImage(cat.name)}
|
src={cat.imageUrl || getCategoryImage(typeof cat.name === 'string' ? cat.name : (cat.name as any)?.en || '')}
|
||||||
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||||
alt={cat.name}
|
alt={typeof cat.name === 'string' ? cat.name : (cat.name as any)?.en || ''}
|
||||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-6 left-6 right-6 z-20 text-white">
|
<div className="absolute bottom-6 left-6 right-6 z-20 text-white">
|
||||||
<span className="font-semibold text-xl block mb-1">
|
<span className="font-semibold text-xl block mb-1">
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (cat.name as any)?.en || ''}
|
||||||
</span>
|
</span>
|
||||||
{cat._count && (
|
{cat._count && (
|
||||||
<span className="text-sm font-medium text-white/80">
|
<span className="text-sm font-medium text-white/80">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
|||||||
<div className="flex flex-col space-y-2 text-sm text-slate-400">
|
<div className="flex flex-col space-y-2 text-sm text-slate-400">
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-white transition-colors">
|
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-white transition-colors">
|
||||||
{c.name}
|
{typeof c.name === 'string' ? c.name : (c.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (c.name as any)?.en || ''}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
|||||||
to={`/category/${cat.id}`}
|
to={`/category/${cat.id}`}
|
||||||
className="flex items-center justify-between px-4 py-2.5 text-xs uppercase tracking-widest text-spa-stone rounded-lg hover:bg-spa-sage hover:text-spa-forest transition-colors"
|
className="flex items-center justify-between px-4 py-2.5 text-xs uppercase tracking-widest text-spa-stone rounded-lg hover:bg-spa-sage hover:text-spa-forest transition-colors"
|
||||||
>
|
>
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''}
|
||||||
<svg className="w-3 h-3 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-3 h-3 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -456,7 +456,7 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
|||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-spa-stone hover:text-spa-forest hover:bg-spa-sage transition-all"
|
className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-spa-stone hover:text-spa-forest hover:bg-spa-sage transition-all"
|
||||||
>
|
>
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''}
|
||||||
<svg className="w-3 h-3 text-spa-stone/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-3 h-3 text-spa-stone/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -541,7 +541,7 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }
|
|||||||
<p className="text-xs text-spa-stone/60 uppercase tracking-widest mb-3 font-semibold">Quick Links</p>
|
<p className="text-xs text-spa-stone/60 uppercase tracking-widest mb-3 font-semibold">Quick Links</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{[
|
{[
|
||||||
...categories.map(c => ({ label: c.name, to: `/category/${c.id}` })),
|
...categories.map(c => ({ label: typeof c.name === 'string' ? c.name : (c.name as any)?.[i18n.language] || (c.name as any)?.en || '', to: `/category/${c.id}` })),
|
||||||
{ label: '✨ Scent Guide', to: '/scent-guide' },
|
{ label: '✨ Scent Guide', to: '/scent-guide' },
|
||||||
{ label: 'All Products', to: '/shop' },
|
{ label: 'All Products', to: '/shop' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
|||||||
{/* Image Area */}
|
{/* Image Area */}
|
||||||
<div className="h-48 md:h-64 overflow-hidden rounded-xl relative bg-spa-sage/20">
|
<div className="h-48 md:h-64 overflow-hidden rounded-xl relative bg-spa-sage/20">
|
||||||
<img
|
<img
|
||||||
src={product.images?.[0]?.url || getCategoryImage(product.category.name)}
|
src={product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||||
alt={product.scent.name}
|
alt={product.scent.name}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
@@ -58,7 +58,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
|||||||
/>
|
/>
|
||||||
{/* Category badge */}
|
{/* Category badge */}
|
||||||
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
|
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
|
||||||
{product.category.name}
|
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
</div>
|
</div>
|
||||||
{/* Wishlist Button */}
|
{/* Wishlist Button */}
|
||||||
<button
|
<button
|
||||||
@@ -79,7 +79,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
|||||||
{displayName}
|
{displayName}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[10px] md:text-xs text-spa-stone/60 mt-1 uppercase tracking-widest line-clamp-1 font-semibold">
|
<p className="text-[10px] md:text-xs text-spa-stone/60 mt-1 uppercase tracking-widest line-clamp-1 font-semibold">
|
||||||
{product.category.name}
|
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
|||||||
price: selectedVariant.price || 0,
|
price: selectedVariant.price || 0,
|
||||||
productName: displayName,
|
productName: displayName,
|
||||||
variantSize: selectedVariant.size,
|
variantSize: selectedVariant.size,
|
||||||
productImage: product.images?.[0]?.url || getCategoryImage(product.category.name)
|
productImage: product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full py-2.5 bg-spa-forest text-white hover:bg-spa-forest/90 rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-sm hover:shadow flex items-center justify-center gap-1.5 cursor-pointer"
|
className="w-full py-2.5 bg-spa-forest text-white hover:bg-spa-forest/90 rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-sm hover:shadow flex items-center justify-center gap-1.5 cursor-pointer"
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ export default function CategoryPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="relative h-64 md:h-80 overflow-hidden">
|
<div className="relative h-64 md:h-80 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={getCategoryImage(category?.name ?? '')}
|
src={getCategoryImage(typeof category?.name === 'string' ? category.name : (category?.name as any)?.en || '')}
|
||||||
className="absolute inset-0 w-full h-full object-cover brightness-30 scale-105"
|
className="absolute inset-0 w-full h-full object-cover brightness-30 scale-105"
|
||||||
alt={category?.name}
|
alt={typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''}
|
||||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-night-900/40" />
|
<div className="absolute inset-0 bg-night-900/40" />
|
||||||
@@ -136,13 +136,13 @@ export default function CategoryPage() {
|
|||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link to="/shop" className="hover:text-white transition-colors">Shop</Link>
|
<Link to="/shop" className="hover:text-white transition-colors">Shop</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-white">{category?.name ?? '...'}</span>
|
<span className="text-white">{typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (category?.name as any)?.en || '...'}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-white">
|
<h1 className="text-4xl md:text-5xl font-bold text-white">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="h-10 w-48 bg-white/20 animate-pulse rounded-lg" />
|
<div className="h-10 w-48 bg-white/20 animate-pulse rounded-lg" />
|
||||||
) : (
|
) : (
|
||||||
category?.name
|
typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{!loading && category?._count && (
|
{!loading && category?._count && (
|
||||||
@@ -169,7 +169,7 @@ export default function CategoryPage() {
|
|||||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (cat.name as any)?.en || ''}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default function ProductDetailPage() {
|
|||||||
price: currentVariant.price || product.price || 0,
|
price: currentVariant.price || product.price || 0,
|
||||||
productName: displayName,
|
productName: displayName,
|
||||||
variantSize: currentVariant.size,
|
variantSize: currentVariant.size,
|
||||||
productImage: product.images?.[0]?.url || getCategoryImage(product.category.name)
|
productImage: product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export default function ProductDetailPage() {
|
|||||||
<Link to="/shop" className="hover:text-spa-forest transition-colors">{t('common.shop')}</Link>
|
<Link to="/shop" className="hover:text-spa-forest transition-colors">{t('common.shop')}</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link to={`/category/${product.category.id}`} className="hover:text-spa-forest transition-colors">
|
<Link to={`/category/${product.category.id}`} className="hover:text-spa-forest transition-colors">
|
||||||
{product.category.name}
|
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-spa-forest">{displayName}</span>
|
<span className="text-spa-forest">{displayName}</span>
|
||||||
@@ -172,9 +172,9 @@ export default function ProductDetailPage() {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ProductImageGallery
|
<ProductImageGallery
|
||||||
images={product.images || []}
|
images={product.images || []}
|
||||||
fallback={getCategoryImage(product.category.name)}
|
fallback={getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')}
|
||||||
alt={product.scent.name}
|
alt={product.scent.name}
|
||||||
badgeText={product.category.name}
|
badgeText={typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ export default function ProductDetailPage() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-spa-forest uppercase tracking-[0.3em] text-xs font-bold opacity-80">
|
<p className="text-spa-forest uppercase tracking-[0.3em] text-xs font-bold opacity-80">
|
||||||
{product.category.name}
|
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-5xl font-bold leading-tight text-pop-black">
|
<h1 className="text-5xl font-bold leading-tight text-pop-black">
|
||||||
{displayName}
|
{displayName}
|
||||||
@@ -258,7 +258,7 @@ export default function ProductDetailPage() {
|
|||||||
to={`/category/${product.category.id}`}
|
to={`/category/${product.category.id}`}
|
||||||
className="text-pop-black font-bold hover:text-spa-forest transition-colors"
|
className="text-pop-black font-bold hover:text-spa-forest transition-colors"
|
||||||
>
|
>
|
||||||
{product.category.name}
|
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,10 +126,10 @@ export default function SearchPage() {
|
|||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<Link
|
<Link
|
||||||
key={cat.id}
|
key={cat.id}
|
||||||
to={`/search?q=${encodeURIComponent(cat.name)}`}
|
to={`/search?q=${encodeURIComponent(typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || '')}`}
|
||||||
className="bg-white border border-spa-stone/20 px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all duration-300"
|
className="bg-white border border-spa-stone/20 px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all duration-300"
|
||||||
>
|
>
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export default function ShopPage() {
|
|||||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cat.name}
|
{typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''}
|
||||||
{cat._count && (
|
{cat._count && (
|
||||||
<span className={`ml-2 ${activeCategory === cat.id ? 'opacity-80' : 'opacity-60'}`}>({cat._count.products})</span>
|
<span className={`ml-2 ${activeCategory === cat.id ? 'opacity-80' : 'opacity-60'}`}>({cat._count.products})</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user