diff --git a/apps/website/src/components/Navbar.tsx b/apps/website/src/components/Navbar.tsx index 6cca20a..8b7b460 100644 --- a/apps/website/src/components/Navbar.tsx +++ b/apps/website/src/components/Navbar.tsx @@ -1,59 +1,155 @@ -"use client"; +"use client" + import Link from 'next/link' import { useRouter, usePathname } from 'next/navigation' -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo, useRef } from 'react' import type { Category } from '@homaroma/types' -import { Heart, ShoppingBag, MapPin, User } from 'lucide-react' +import { Check, Globe, Heart, Menu as MenuIcon, ShoppingBag, User, X } from 'lucide-react' import { useCartStore } from '../store/cartStore' import { useWishlistStore } from '../store/wishlistStore' import { useAuthStore } from '../store/authStore' import { useTranslation } from 'react-i18next' import { resolveLocalized } from '../lib/localization' +import { API_BASE } from '../lib/api' +import { DesktopMegaMenu } from './navigation/DesktopMegaMenu' +import { MobileDrawer } from './navigation/MobileDrawer' +import type { MegaMenuPayload } from './navigation/menuTypes' interface NavbarProps { categories?: Category[] } +const LANGUAGES = [ + { code: 'th', label: 'ไทย', short: 'TH' }, + { code: 'en', label: 'English', short: 'EN' }, + { code: 'ja', label: '日本語', short: 'JP' }, + { code: 'zh', label: '简体中文', short: 'ZH' }, + { code: 'vi', label: 'Tiếng Việt', short: 'VN' }, + { code: 'ko', label: '한국어', short: 'KO' }, +] + +function createFallbackMenu(categories: Category[]): MegaMenuPayload[] { + const categoryLinks = categories.map((category) => ({ + label: { + th: resolveLocalized(category.name, 'th') || resolveLocalized(category.name), + en: resolveLocalized(category.name, 'en') || resolveLocalized(category.name), + }, + url: `/category/${category.id}`, + })) + + return [ + { id: 'home', label: { th: 'หน้าหลัก', en: 'Home' }, url: '/' }, + { + id: 'shop', + label: { th: 'ร้านค้า', en: 'Shop' }, + url: '/shop', + columns: categoryLinks.length + ? [ + { + title: { th: 'คอลเลกชัน', en: 'Collections' }, + links: categoryLinks, + }, + ] + : undefined, + }, + { id: 'promotions', label: { th: 'โปรโมชัน', en: 'Promotions' }, url: '/coupons' }, + { id: 'scent-guide', label: { th: 'คลาสแนะนำกลิ่น', en: 'Scent Guide' }, url: '/scent-guide' }, + { id: 'about', label: { th: 'เกี่ยวกับเรา', en: 'About' }, url: '/about' }, + ] +} + export default function Navbar({ categories = [] }: NavbarProps) { const [searchOpen, setSearchOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false) + const [langSheetOpen, setLangSheetOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [scrolled, setScrolled] = useState(false) - const [localOrdersCount, setLocalOrdersCount] = useState(0) + const [megaMenu, setMegaMenu] = useState([]) + const [menuLoaded, setMenuLoaded] = useState(false) + const navRef = useRef(null) const router = useRouter() const pathname = usePathname() const { toggleCart, getCartCount } = useCartStore() const wishlistCount = useWishlistStore((state) => state.items.length) const customer = useAuthStore((state) => state.customer) + const logout = useAuthStore((state) => state.logout) const cartCount = getCartCount() const { t, i18n } = useTranslation() + const lang = i18n.language ? i18n.language.substring(0, 2) : 'th' - const toggleLanguage = () => { - const newLang = i18n.language.startsWith('en') ? 'th' : 'en' - i18n.changeLanguage(newLang) - localStorage.setItem('homaroma_lang', newLang) + const fallbackMenu = useMemo(() => createFallbackMenu(categories), [categories]) + const menuItems = menuLoaded && megaMenu.length > 0 ? megaMenu : fallbackMenu + + const changeLanguage = (code: string) => { + i18n.changeLanguage(code) + localStorage.setItem('homaroma_lang', code) + setLangSheetOpen(false) } + const loginWithLine = () => { + const maybeWindow = window as Window & { liff?: { login: () => void } } + if (maybeWindow.liff?.login) { + maybeWindow.liff.login() + return + } + router.push('/profile') + } + + useEffect(() => { + let isMounted = true + + async function fetchMegaMenu() { + try { + const response = await fetch(`${API_BASE}/menus/main`) + if (!response.ok) throw new Error(`menus/main returned ${response.status}`) + const data = await response.json() as { payload?: MegaMenuPayload[] } + if (isMounted) { + setMegaMenu(Array.isArray(data.payload) ? data.payload : []) + setMenuLoaded(true) + } + } catch (error) { + console.error('[website-menu] Failed to fetch main menu', error) + if (isMounted) setMenuLoaded(true) + } + } + + fetchMegaMenu() + + return () => { + isMounted = false + } + }, []) + useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 20) window.addEventListener('scroll', onScroll, { passive: true }) - - const stored = localStorage.getItem('homaroma_local_orders') - if (stored) { - try { - const parsed = JSON.parse(stored) as unknown[] - queueMicrotask(() => setLocalOrdersCount(parsed.length)) - } catch {} - } - + onScroll() return () => window.removeEventListener('scroll', onScroll) }, []) useEffect(() => { - document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : '' + const updateNavBottom = () => { + if (navRef.current) { + const bottom = navRef.current.getBoundingClientRect().bottom + document.documentElement.style.setProperty('--navbar-bottom', `${bottom}px`) + } + } + + updateNavBottom() + window.addEventListener('scroll', updateNavBottom, { passive: true }) + window.addEventListener('resize', updateNavBottom) + + return () => { + window.removeEventListener('scroll', updateNavBottom) + window.removeEventListener('resize', updateNavBottom) + } + }, []) + + useEffect(() => { + document.body.style.overflow = (searchOpen || mobileOpen || langSheetOpen) ? 'hidden' : '' return () => { document.body.style.overflow = '' } - }, [searchOpen, mobileOpen]) + }, [searchOpen, mobileOpen, langSheetOpen]) const handleSearch = (e: React.FormEvent) => { e.preventDefault() @@ -65,230 +161,150 @@ export default function Navbar({ categories = [] }: NavbarProps) { } } - const navLinkClass = (path: string) => - pathname === path - ? 'text-pop-pink' - : 'text-pop-black/75 transition-colors hover:text-pop-pink' - - const iconButtonClass = 'rounded-full border border-gray-200 bg-white p-2.5 text-pop-black shadow-sm transition hover:border-pop-pink hover:text-pop-pink hover:shadow-brutal' + const iconButtonClass = 'rounded-full p-1.5 text-pop-black/65 transition-all hover:bg-pop-bg hover:text-pop-pink md:p-2' + const activeLanguage = LANGUAGES.find((language) => i18n.language.startsWith(language.code)) ?? LANGUAGES[0] return ( <> -
-
setMobileOpen(false)} - /> -
-
- {t('nav.menu')} -
- - -
-
- -
- {[ - { to: '/', label: t('nav.home') }, - { to: '/shop', label: t('nav.shop') }, - { to: '/coupons', label: `🔥 ${t('nav.promotions') || 'Promotions'}` }, - { to: '/wishlist', label: '♡ Wishlist' }, - { to: '/profile', label: customer ? `👤 ${customer.displayName || 'Profile'}` : '👤 Profile' }, - { to: '/scent-guide', label: `✨ ${t('nav.scentGuide')}` }, - { to: '/about', label: t('nav.about') }, - { to: '/faq', label: t('nav.faq') }, - { to: '/contact', label: t('nav.contact') }, - ].map(item => ( - setMobileOpen(false)} - className={`block rounded-xl px-4 py-3 text-sm font-semibold tracking-wide transition-all duration-200 ${ - pathname === item.to ? 'border border-pop-pink/20 bg-pop-bg text-pop-pink' : 'text-pop-black/75 hover:bg-pop-bg hover:text-pop-pink' - }`} - > - {item.label} - - ))} - - {categories.length > 0 && ( -
-

{t('nav.collections')}

- {categories.map(cat => ( - setMobileOpen(false)} - className="flex items-center justify-between rounded-xl px-4 py-2.5 text-sm text-gray-600 transition-all hover:bg-pop-bg hover:text-pop-pink" - > - {resolveLocalized(cat.name)} - - - - - ))} -
- )} -
- -
- setMobileOpen(false)} className="block w-full rounded-xl bg-pop-black py-3 text-center text-xs font-bold uppercase tracking-widest text-white transition hover:bg-pop-pink"> - {t('nav.shopNow')} - - setMobileOpen(false)} className="block w-full rounded-xl border border-pop-pink/30 bg-white py-3 text-center text-xs font-bold uppercase tracking-widest text-pop-pink transition hover:bg-pop-bg"> - {t('nav.findMyScent')} - -
-
-
+ setMobileOpen(false)} + onOpenLang={() => setLangSheetOpen(true)} + onLogin={loginWithLine} + onLogout={logout} + /> {searchOpen && (
-

{t('nav.search.title')}

-
-
- - +
+ + setSearchQuery(e.target.value)} - placeholder="Search scents, categories..." - className="w-full border-none bg-transparent p-4 pl-0 text-lg text-pop-black outline-none placeholder:text-gray-400 md:text-xl" + placeholder={t('nav.search.placeholder')} + className="w-full border-none bg-transparent p-4 pl-0 text-lg text-pop-black outline-none placeholder:text-pop-black/35 md:text-xl" autoFocus /> +
+
+ {LANGUAGES.map((language) => { + const isActive = i18n.language.startsWith(language.code) + return ( + + ) + })} +
+
+
+
) } diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index f3e82ea..cea1aab 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -49,6 +49,19 @@ function renderAccentTitle(text: string, accentClassName = 'text-pop-pink italic }) } +function heroLearnMoreText(lang: string) { + const labels: Record = { + th: 'ดูรายละเอียดเพิ่มเติม', + en: 'Learn More', + ja: '詳しく見る', + zh: '了解更多', + vi: 'Tìm hiểu thêm', + ko: '자세히 보기', + } + + return labels[lang] ?? labels.en +} + function AnnouncementBar({ payload, lang }: { payload: Record, lang: string }) { const slides = Array.isArray(payload.slides) ? payload.slides : [] if (!slides.length) return null @@ -74,31 +87,44 @@ function HeroSection({ payload, lang }: { payload: Record, lang const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop' return ( -
+
{title -
-
-
+
+
+
+
{tagline && ( -

+

{tagline}

)} -

- {title || 'HomAroma'} +

+ {renderAccentTitle(title || 'HomAroma', 'font-medium italic text-[#E4BC32]')}

{description && ( -

+

{description}

)} - - {buttonText || 'Explore Collection'} - +
+ + {buttonText || 'Explore Collection'} + + + {heroLearnMoreText(lang)} + → + +
diff --git a/apps/website/src/components/navigation/DesktopMegaMenu.tsx b/apps/website/src/components/navigation/DesktopMegaMenu.tsx new file mode 100644 index 0000000..dfb7ae7 --- /dev/null +++ b/apps/website/src/components/navigation/DesktopMegaMenu.tsx @@ -0,0 +1,102 @@ +"use client" + +import Link from 'next/link' +import { useState, useRef } from 'react' +import type { MegaMenuPayload } from './menuTypes' +import { getLocalizedMenuText } from './menuTypes' + +interface DesktopMegaMenuProps { + items: MegaMenuPayload[] + lang: string + currentPath: string +} + +export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuProps) { + const [openId, setOpenId] = useState(null) + const closeTimer = useRef | null>(null) + + const handleOpen = (id: string) => { + if (closeTimer.current) clearTimeout(closeTimer.current) + setOpenId(id) + } + + const handleClose = () => { + closeTimer.current = setTimeout(() => setOpenId(null), 150) + } + + return ( +
+ {items.map((item, index) => { + const uniqueId = item.nodeId || item.id || `menu-${item.url || index}` + const isOpen = openId === uniqueId + const hasColumns = !!item.columns?.length + const isActive = item.url ? currentPath === item.url : false + + return ( +
handleOpen(uniqueId)} + onMouseLeave={handleClose} + > + + {getLocalizedMenuText(item.label, lang)} + {hasColumns && ( + + + + )} + + + {hasColumns && ( +
handleOpen(uniqueId)} + onMouseLeave={handleClose} + > +
+
+ {item.columns!.map((column, columnIndex) => ( +
+

+ {getLocalizedMenuText(column.title, lang)} +

+
    + {column.links.map((link, linkIndex) => ( +
  • + + {getLocalizedMenuText(link.label, lang)} + +
  • + ))} +
+
+ ))} +
+
+
+ )} +
+ ) + })} +
+ ) +} diff --git a/apps/website/src/components/navigation/MobileDrawer.tsx b/apps/website/src/components/navigation/MobileDrawer.tsx new file mode 100644 index 0000000..c8bcbe3 --- /dev/null +++ b/apps/website/src/components/navigation/MobileDrawer.tsx @@ -0,0 +1,245 @@ +"use client" + +import { useEffect, useState } from 'react' +import Link from 'next/link' +import { ChevronLeft, ChevronRight, Globe, X } from 'lucide-react' +import type { MegaMenuPayload, MenuColumn, MenuLink } from './menuTypes' +import { getLocalizedMenuText } from './menuTypes' + +type MenuNode = + | { type: 'root'; data: MegaMenuPayload[]; title: string | Record } + | { type: 'mega'; data: MenuColumn[]; title: string | Record } + | { type: 'links'; data: MenuLink[]; title: string | Record } + +interface MobileDrawerProps { + isOpen: boolean + items: MegaMenuPayload[] + lang: string + customerName?: string | null + onClose: () => void + onOpenLang: () => void + onLogin: () => void + onLogout: () => void +} + +export function MobileDrawer({ + isOpen, + items, + lang, + customerName, + onClose, + onOpenLang, + onLogin, + onLogout, +}: MobileDrawerProps) { + const [stack, setStack] = useState([{ type: 'root', data: items, title: 'Menu' }]) + + useEffect(() => { + if (!isOpen) { + const timer = setTimeout(() => { + setStack([{ type: 'root', data: items, title: 'Menu' }]) + }, 300) + return () => clearTimeout(timer) + } + + const timer = setTimeout(() => { + setStack((current) => { + if (current.length === 1 && current[0].data !== items) { + return [{ type: 'root', data: items, title: 'Menu' }] + } + return current + }) + }, 0) + + return () => clearTimeout(timer) + }, [isOpen, items]) + + const pushView = (node: MenuNode) => setStack((prev) => [...prev, node]) + const popView = () => setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev)) + + const linkClass = 'block w-full py-4 text-left text-sm font-medium uppercase tracking-wide text-pop-black/65 transition-colors hover:text-pop-pink' + const nestedLinkClass = 'block w-full py-2 text-left text-xs uppercase tracking-widest text-pop-black/65 transition-colors hover:text-pop-pink' + + const renderView = (view: MenuNode) => { + if (view.type === 'root') { + return ( +
+ {view.data.map((item, index) => ( +
+ {item.columns?.length ? ( + + ) : ( + + {getLocalizedMenuText(item.label, lang)} + + )} +
+ ))} + {customerName && ( +
+ + My Profile + +
+ )} +
+ ) + } + + if (view.type === 'mega') { + return ( +
+ {view.data.map((column, columnIndex) => ( +
+

+ {getLocalizedMenuText(column.title, lang)} +

+
+ {column.links.map((link, linkIndex) => ( +
+ {link.links?.length ? ( + + ) : ( + + {getLocalizedMenuText(link.label, lang)} + + )} +
+ ))} +
+
+ ))} +
+ ) + } + + return ( +
+ {view.data.map((link, linkIndex) => ( +
+ {link.links?.length ? ( + + ) : ( + + {getLocalizedMenuText(link.label, lang)} + + )} +
+ ))} +
+ ) + } + + return ( +
+
+
+
+ Menu +
+ + +
+
+ +
+ {stack.map((view, index) => { + const isCurrent = index === stack.length - 1 + const isPast = index < stack.length - 1 + + return ( +
+ {index > 0 && ( + + )} + + {index > 0 && ( +
+

{getLocalizedMenuText(view.title, lang)}

+
+ )} + +
{renderView(view)}
+
+ ) + })} +
+ +
+ + Shop Now + + {customerName ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/apps/website/src/components/navigation/menuTypes.ts b/apps/website/src/components/navigation/menuTypes.ts new file mode 100644 index 0000000..ad3737d --- /dev/null +++ b/apps/website/src/components/navigation/menuTypes.ts @@ -0,0 +1,24 @@ +export interface MenuLink { + label: Record | string + url?: string + links?: MenuLink[] +} + +export interface MenuColumn { + title: Record | string + links: MenuLink[] +} + +export interface MegaMenuPayload { + id?: string + nodeId?: string + label: Record | string + url?: string + columns?: MenuColumn[] +} + +export function getLocalizedMenuText(text: Record | string | undefined, lang: string) { + if (typeof text === 'string') return text + if (!text) return '' + return text[lang] || text.en || text.th || Object.values(text)[0] || '' +} diff --git a/apps/website/src/lib/i18n.ts b/apps/website/src/lib/i18n.ts index 70fcf07..24a3e6f 100644 --- a/apps/website/src/lib/i18n.ts +++ b/apps/website/src/lib/i18n.ts @@ -3,6 +3,10 @@ import { initReactI18next } from 'react-i18next' import enTranslation from '../locales/en/translation.json' import thTranslation from '../locales/th/translation.json' +import jaTranslation from '../locales/ja/translation.json' +import zhTranslation from '../locales/zh/translation.json' +import viTranslation from '../locales/vi/translation.json' +import koTranslation from '../locales/ko/translation.json' // Ensure we only access localStorage on the client side const savedLang = typeof window !== 'undefined' ? (localStorage.getItem('homaroma_lang') || 'th') : 'th' @@ -15,6 +19,10 @@ if (!i18n.isInitialized) { resources: { en: { translation: enTranslation }, th: { translation: thTranslation }, + ja: { translation: jaTranslation }, + zh: { translation: zhTranslation }, + vi: { translation: viTranslation }, + ko: { translation: koTranslation }, }, lng: savedLang, // default language fallbackLng: 'en', diff --git a/apps/website/src/locales/ja/translation.json b/apps/website/src/locales/ja/translation.json new file mode 100644 index 0000000..cd7752e --- /dev/null +++ b/apps/website/src/locales/ja/translation.json @@ -0,0 +1,61 @@ +{ + "nav": { + "home": "ホーム", + "shop": "ショップ", + "collections": "コレクション", + "scentGuide": "香りガイド", + "about": "私たちについて", + "menu": "メニュー", + "faq": "よくある質問", + "contact": "お問い合わせ", + "shopNow": "今すぐ購入", + "findMyScent": "香りを探す ✨", + "promotions": "プロモーション", + "profile": "マイプロフィール", + "search": { + "title": "検索", + "placeholder": "香りを検索...", + "quickLinks": "クイックリンク", + "allProducts": "すべての商品" + }, + "trackOrder": "注文追跡", + "delivery": "配送と返品" + }, + "cart": { + "title": "カート", + "items": "{{count}} 点", + "empty": "カートは空です。", + "continueShopping": "ショッピングを続ける", + "subtotal": "小計", + "shippingNote": "送料と税金はチェックアウト時に計算されます。", + "proceed": "レジへ進む", + "addToCart": "カートに追加", + "totalPrice": "合計金額" + }, + "footer": { + "catchphrase": "プレミアムな香りで空間をアップグレード", + "explore": "探索", + "support": "サポート", + "followUs": "フォローする", + "contactUs": "お問い合わせ", + "rights": "All rights reserved." + }, + "common": { + "home": "ホーム", + "shop": "ショップ", + "viewAll": "すべて見る →", + "backToShop": "ショップに戻る" + }, + "product": { + "notFound": "商品が見つかりません", + "premiumCollection": "プレミアムコレクション", + "selectSize": "サイズを選択", + "details": "商品詳細", + "scent": "香り", + "category": "カテゴリー", + "relatedProducts": "関連商品", + "moreRelated": "関連する {{category}}", + "reviews": "レビュー", + "viewDetails": "詳細を見る →" + } +} diff --git a/apps/website/src/locales/ko/translation.json b/apps/website/src/locales/ko/translation.json new file mode 100644 index 0000000..8673611 --- /dev/null +++ b/apps/website/src/locales/ko/translation.json @@ -0,0 +1,61 @@ +{ + "nav": { + "home": "홈", + "shop": "쇼핑", + "collections": "컬렉션", + "scentGuide": "향기 가이드", + "about": "소개", + "menu": "메뉴", + "faq": "자주 묻는 질문", + "contact": "연락처", + "shopNow": "지금 쇼핑하기", + "findMyScent": "나의 향기 찾기 ✨", + "promotions": "프로모션", + "profile": "내 프로필", + "search": { + "title": "검색", + "placeholder": "향기 검색...", + "quickLinks": "빠른 링크", + "allProducts": "모든 제품" + }, + "trackOrder": "주문 배송조회", + "delivery": "배송 및 반품" + }, + "cart": { + "title": "장바구니", + "items": "{{count}} 개", + "empty": "장바구니가 비어 있습니다.", + "continueShopping": "쇼핑 계속하기", + "subtotal": "소계", + "shippingNote": "배송비 및 세금은 결제 시 계산됩니다.", + "proceed": "결제 진행", + "addToCart": "장바구니 담기", + "totalPrice": "총 가격" + }, + "footer": { + "catchphrase": "프리미엄 향기로 공간을 업그레이드하세요.", + "explore": "탐색", + "support": "지원", + "followUs": "팔로우", + "contactUs": "연락처", + "rights": "모든 권리 보유." + }, + "common": { + "home": "홈", + "shop": "쇼핑", + "viewAll": "모두 보기 →", + "backToShop": "쇼핑으로 돌아가기" + }, + "product": { + "notFound": "상품을 찾을 수 없습니다", + "premiumCollection": "프리미엄 컬렉션", + "selectSize": "사이즈 선택", + "details": "상품 상세 정보", + "scent": "향기", + "category": "카테고리", + "relatedProducts": "관련 상품", + "moreRelated": "더 많은 {{category}}", + "reviews": "리뷰", + "viewDetails": "상세 보기 →" + } +} diff --git a/apps/website/src/locales/vi/translation.json b/apps/website/src/locales/vi/translation.json new file mode 100644 index 0000000..12a757e --- /dev/null +++ b/apps/website/src/locales/vi/translation.json @@ -0,0 +1,61 @@ +{ + "nav": { + "home": "Trang chủ", + "shop": "Cửa hàng", + "collections": "Bộ sưu tập", + "scentGuide": "Hướng dẫn mùi hương", + "about": "Giới thiệu", + "menu": "Menu", + "faq": "Câu hỏi thường gặp", + "contact": "Liên hệ", + "shopNow": "Mua ngay", + "findMyScent": "Tìm mùi hương của bạn ✨", + "promotions": "Khuyến mãi", + "profile": "Hồ sơ của tôi", + "search": { + "title": "Tìm kiếm", + "placeholder": "Tìm mùi hương...", + "quickLinks": "Liên kết nhanh", + "allProducts": "Tất cả sản phẩm" + }, + "trackOrder": "Theo dõi đơn hàng", + "delivery": "Giao hàng & Hoàn trả" + }, + "cart": { + "title": "Giỏ hàng", + "items": "{{count}} sản phẩm", + "empty": "Giỏ hàng của bạn đang trống.", + "continueShopping": "Tiếp tục mua sắm", + "subtotal": "Tổng phụ", + "shippingNote": "Phí vận chuyển và thuế được tính lúc thanh toán.", + "proceed": "Tiến hành thanh toán", + "addToCart": "Thêm vào giỏ", + "totalPrice": "Tổng giá" + }, + "footer": { + "catchphrase": "Nâng tầm không gian với hương thơm cao cấp.", + "explore": "Khám phá", + "support": "Hỗ trợ", + "followUs": "Theo dõi chúng tôi", + "contactUs": "Liên hệ", + "rights": "Bảo lưu mọi quyền." + }, + "common": { + "home": "Trang chủ", + "shop": "Cửa hàng", + "viewAll": "Xem tất cả →", + "backToShop": "Quay lại cửa hàng" + }, + "product": { + "notFound": "Không tìm thấy sản phẩm", + "premiumCollection": "Bộ sưu tập Cao cấp", + "selectSize": "Chọn kích thước", + "details": "Chi tiết sản phẩm", + "scent": "Mùi hương", + "category": "Danh mục", + "relatedProducts": "Sản phẩm liên quan", + "moreRelated": "Thêm {{category}}", + "reviews": "Đánh giá", + "viewDetails": "Xem chi tiết →" + } +} diff --git a/apps/website/src/locales/zh/translation.json b/apps/website/src/locales/zh/translation.json new file mode 100644 index 0000000..d19e23e --- /dev/null +++ b/apps/website/src/locales/zh/translation.json @@ -0,0 +1,61 @@ +{ + "nav": { + "home": "首页", + "shop": "商店", + "collections": "系列", + "scentGuide": "香气指南", + "about": "关于我们", + "menu": "菜单", + "faq": "常见问题", + "contact": "联系我们", + "shopNow": "立即购买", + "findMyScent": "寻找我的香气 ✨", + "promotions": "促销", + "profile": "我的主页", + "search": { + "title": "搜索", + "placeholder": "搜索香气...", + "quickLinks": "快速链接", + "allProducts": "所有商品" + }, + "trackOrder": "追踪订单", + "delivery": "配送与退货" + }, + "cart": { + "title": "购物车", + "items": "{{count}} 件", + "empty": "您的购物车是空的。", + "continueShopping": "继续购物", + "subtotal": "小计", + "shippingNote": "运费和税费将在结账时计算。", + "proceed": "去结账", + "addToCart": "加入购物车", + "totalPrice": "总价" + }, + "footer": { + "catchphrase": "用高级香氛提升您的空间。", + "explore": "探索", + "support": "支持", + "followUs": "关注我们", + "contactUs": "联系我们", + "rights": "版权所有。" + }, + "common": { + "home": "首页", + "shop": "商店", + "viewAll": "查看全部 →", + "backToShop": "返回商店" + }, + "product": { + "notFound": "未找到商品", + "premiumCollection": "高级系列", + "selectSize": "选择尺寸", + "details": "商品详情", + "scent": "香气", + "category": "类别", + "relatedProducts": "相关商品", + "moreRelated": "更多 {{category}}", + "reviews": "评价", + "viewDetails": "查看详情 →" + } +}