feat: implement i18n for product and category, add 5 new locales

This commit is contained in:
Your Name committed 2026-06-13 23:58:50 -04:00
1 parent b867063efb
commit 96d874233d
17 files changed
+557 -62

No files matched your search

+27 -19
View File
@@ -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>