feat: implement i18n for product and category, add 5 new locales
This commit is contained in:
1 parent
b867063efb
commit
96d874233d
17 files changed
+557
-62
No files matched your search
@@ -30,7 +30,15 @@ export default function ProductDetailPage() {
|
||||
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
||||
|
||||
const { addToCart } = useCartStore()
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
function resolveStr(v: TranslatableString | null | undefined, lang: string): string {
|
||||
if (!v) return ''
|
||||
if (typeof v === 'string') return v
|
||||
return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? ''
|
||||
}
|
||||
|
||||
const displayName = resolveStr(product?.name, i18n.language) || product?.scent?.name || ''
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
||||
@@ -104,9 +112,9 @@ export default function ProductDetailPage() {
|
||||
<Navbar categories={categories} />
|
||||
<div className="pt-40 text-center space-y-4">
|
||||
<p className="text-4xl">⚠️</p>
|
||||
<p className="text-spa-stone font-medium">{error ?? 'Product not found'}</p>
|
||||
<p className="text-spa-stone font-medium">{error ?? t('product.notFound')}</p>
|
||||
<Link to="/shop" className="text-spa-forest hover:underline text-sm font-bold">
|
||||
Back to Shop
|
||||
{t('common.backToShop')}
|
||||
</Link>
|
||||
</div>
|
||||
<Footer categories={categories} />
|
||||
@@ -123,7 +131,7 @@ export default function ProductDetailPage() {
|
||||
variantId: currentVariant.id,
|
||||
quantity: 1,
|
||||
price: currentVariant.price || product.price || 0,
|
||||
productName: product.scent.name,
|
||||
productName: displayName,
|
||||
variantSize: currentVariant.size,
|
||||
productImage: product.images?.[0]?.url || getCategoryImage(product.category.name)
|
||||
})
|
||||
@@ -147,15 +155,15 @@ export default function ProductDetailPage() {
|
||||
<div className="max-w-7xl mx-auto px-6 pt-24 md:pt-32 pb-24 space-y-12 md:space-y-24">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center flex-wrap gap-2 text-[10px] md:text-xs text-spa-stone/60 uppercase tracking-widest font-semibold mb-6 md:mb-0">
|
||||
<Link to="/" className="hover:text-spa-forest transition-colors">Home</Link>
|
||||
<Link to="/" className="hover:text-spa-forest transition-colors">{t('common.home')}</Link>
|
||||
<span>/</span>
|
||||
<Link to="/shop" className="hover:text-spa-forest transition-colors">Shop</Link>
|
||||
<Link to="/shop" className="hover:text-spa-forest transition-colors">{t('common.shop')}</Link>
|
||||
<span>/</span>
|
||||
<Link to={`/category/${product.category.id}`} className="hover:text-spa-forest transition-colors">
|
||||
{product.category.name}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-spa-forest">{product.scent.name}</span>
|
||||
<span className="text-spa-forest">{displayName}</span>
|
||||
</div>
|
||||
|
||||
{/* Product Detail */}
|
||||
@@ -177,10 +185,10 @@ export default function ProductDetailPage() {
|
||||
{product.category.name}
|
||||
</p>
|
||||
<h1 className="text-5xl font-bold leading-tight text-pop-black">
|
||||
{product.scent.name}
|
||||
{displayName}
|
||||
</h1>
|
||||
<p className="text-spa-stone/80 text-sm uppercase tracking-widest font-semibold">
|
||||
Premium Collection
|
||||
{t('product.premiumCollection')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +209,7 @@ export default function ProductDetailPage() {
|
||||
{/* Variant Selector */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">
|
||||
Select Size
|
||||
{t('product.selectSize')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{product.variants.map((v, i) => (
|
||||
@@ -229,7 +237,7 @@ export default function ProductDetailPage() {
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
Add to Cart
|
||||
{t('cart.addToCart')}
|
||||
</button>
|
||||
<button className="bg-white border border-spa-stone/20 text-spa-stone p-4 rounded-lg hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -241,11 +249,11 @@ export default function ProductDetailPage() {
|
||||
{/* Meta */}
|
||||
<div className="border-t border-spa-stone/10 pt-6 grid grid-cols-2 gap-4 text-sm text-spa-stone/80">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">Scent</span>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">{t('product.scent')}</span>
|
||||
<span className="text-pop-black font-bold">{product.scent.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">Category</span>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">{t('product.category')}</span>
|
||||
<Link
|
||||
to={`/category/${product.category.id}`}
|
||||
className="text-pop-black font-bold hover:text-spa-forest transition-colors"
|
||||
@@ -260,7 +268,7 @@ export default function ProductDetailPage() {
|
||||
{/* Full Details Section */}
|
||||
{resolveTranslatable(product.details, i18n.language) && (
|
||||
<div className="pt-16 border-t border-spa-stone/10 space-y-6">
|
||||
<h2 className="text-3xl font-bold text-pop-black">Product Details</h2>
|
||||
<h2 className="text-3xl font-bold text-pop-black">{t('product.details')}</h2>
|
||||
<div
|
||||
className="prose prose-spa prose-lg max-w-4xl product-editorial text-spa-stone"
|
||||
dangerouslySetInnerHTML={{ __html: resolveTranslatable(product.details, i18n.language) }}
|
||||
@@ -277,15 +285,15 @@ export default function ProductDetailPage() {
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<p className="text-spa-forest uppercase tracking-[0.3em] text-xs font-bold opacity-80 mb-2">
|
||||
More {product.category.name}
|
||||
{t('product.moreRelated', { category: typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.['en'] })}
|
||||
</p>
|
||||
<h2 className="text-3xl font-bold text-pop-black">Related Products</h2>
|
||||
<h2 className="text-3xl font-bold text-pop-black">{t('product.relatedProducts')}</h2>
|
||||
</div>
|
||||
<Link
|
||||
to={`/category/${product.category.id}`}
|
||||
className="text-spa-forest text-sm font-bold uppercase tracking-widest hover:underline"
|
||||
>
|
||||
View All →
|
||||
{t('common.viewAll')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
|
||||
@@ -332,7 +340,7 @@ export default function ProductDetailPage() {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase tracking-widest text-spa-stone/60 font-semibold">
|
||||
{product.variants && product.variants.length > 1 ? currentVariant?.size : 'Total Price'}
|
||||
{product.variants && product.variants.length > 1 ? currentVariant?.size : t('cart.totalPrice')}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-rose-500">
|
||||
{formatPrice(currentVariant?.price)}
|
||||
@@ -342,7 +350,7 @@ export default function ProductDetailPage() {
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 py-3.5 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone shadow-sm transition-all flex justify-center items-center gap-2"
|
||||
>
|
||||
Add to Cart
|
||||
{t('cart.addToCart')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user