This commit is contained in:
Your Name committed 2026-07-21 21:22:40 -04:00
1 parent cfd642dee0
commit ae3799d8ce
21 files changed
+172 -30

No files matched your search

@@ -13,6 +13,7 @@ import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
import { resolveCategoryName, resolveLocalized } from '@/lib/localization'
import { SUPPORTED_LOCALES } from '@/lib/locales'
import { extractIdFromSlug, generateProductSlug } from '@/lib/slugify'
import type { ProductAttribute } from '@homaroma/types'
@@ -361,8 +362,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.homaroma.com'
const customLanguages: Record<string, string> = {}
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko']
for (const l of languages) {
for (const l of SUPPORTED_LOCALES) {
const locName = resolveLocalized(product.name, l) || product.brand.name
customLanguages[l] = `${baseUrl}/${l}/products/${generateProductSlug(locName, product.id)}`
}
+9 -10
View File
@@ -2,14 +2,13 @@ import { MetadataRoute } from 'next';
import { getCategories, getProducts, getArticlesResult } from '../lib/cms';
import { generateProductSlug } from '../lib/slugify';
import { resolveLocalized } from '../lib/localization';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../lib/locales';
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
function createAlternates(path: string) {
const alternates: Record<string, string> = {};
for (const lang of languages) {
for (const lang of SUPPORTED_LOCALES) {
alternates[lang] = `${baseUrl}/${lang}${path}`;
}
return alternates;
@@ -32,7 +31,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
];
const routes: MetadataRoute.Sitemap = staticPages.map((route) => ({
url: `${baseUrl}/th${route}`,
url: `${baseUrl}/${DEFAULT_LOCALE}${route}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: route === '' ? 1 : 0.8,
@@ -42,7 +41,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}));
const categoryRoutes: MetadataRoute.Sitemap = categories.map((category) => ({
url: `${baseUrl}/th/category/${category.id}`,
url: `${baseUrl}/${DEFAULT_LOCALE}/category/${category.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
@@ -53,17 +52,17 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const productRoutes: MetadataRoute.Sitemap = products.map((product) => {
const alternates: Record<string, string> = {};
let thSlug = '';
let defaultSlug = '';
for (const lang of languages) {
for (const lang of SUPPORTED_LOCALES) {
const localizedName = resolveLocalized(product.name, lang) || (product.brand ? product.brand.name : 'product');
const slug = generateProductSlug(localizedName, product.id);
alternates[lang] = `${baseUrl}/${lang}/products/${slug}`;
if (lang === 'th') thSlug = slug;
if (lang === DEFAULT_LOCALE) defaultSlug = slug;
}
return {
url: `${baseUrl}/th/products/${thSlug}`,
url: `${baseUrl}/${DEFAULT_LOCALE}/products/${defaultSlug}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.9,
@@ -77,7 +76,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
if (articlesResult.ok && articlesResult.data) {
articlesResult.data.forEach((article) => {
articleRoutes.push({
url: `${baseUrl}/th/article/${article.slug}`,
url: `${baseUrl}/${DEFAULT_LOCALE}/article/${article.slug}`,
lastModified: new Date(article.updatedAt || article.createdAt),
changeFrequency: 'monthly',
priority: 0.6,
@@ -7,13 +7,14 @@ import i18n from '../lib/i18n';
import { useCartStore } from '../store/cartStore';
import { useWishlistStore } from '../store/wishlistStore';
import { useAuthStore } from '../store/authStore';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../lib/locales';
export default function ClientProviders({ children }: { children: ReactNode }) {
useEffect(() => {
// With Locale Routing, the URL is the source of truth for language.
// Sync i18n to match the current URL if needed.
const pathLang = window.location.pathname.split('/')[1];
const targetLang = pathLang && ['th', 'en', 'ja', 'zh', 'vi', 'ko'].includes(pathLang) ? pathLang : 'th';
const targetLang = pathLang && (SUPPORTED_LOCALES as readonly string[]).includes(pathLang) ? pathLang : DEFAULT_LOCALE;
if (targetLang !== i18n.language) {
void i18n.changeLanguage(targetLang);
+4 -2
View File
@@ -2,7 +2,7 @@
import Image from 'next/image'
import Link from './LocalizedLink'
import { getPathWithoutLocale, getPathWithLocale } from '../lib/locales'
import { getPathWithoutLocale, getPathWithLocale, type Locale } from '../lib/locales'
import { useRouter, usePathname } from 'next/navigation'
import { useState, useEffect, useMemo, useRef } from 'react'
import type { Category } from '@homaroma/types'
@@ -31,7 +31,9 @@ const LANGUAGES = [
{ code: 'zh', label: '简体中文', short: 'ZH' },
{ code: 'vi', label: 'Tiếng Việt', short: 'VN' },
{ code: 'ko', label: '한국어', short: 'KO' },
]
{ code: 'lo', label: 'ລາວ', short: 'LO' },
{ code: 'it', label: 'Italiano', short: 'IT' },
] satisfies Array<{ code: Locale; label: string; short: string }>
function setLanguageCookie(code: string) {
if (typeof document !== 'undefined') {
+4
View File
@@ -7,6 +7,8 @@ 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'
import loTranslation from '../locales/lo/translation.json'
import itTranslation from '../locales/it/translation.json'
// Check if it's already initialized to prevent re-initialization on HMR
if (!i18n.isInitialized) {
@@ -20,6 +22,8 @@ if (!i18n.isInitialized) {
zh: { translation: zhTranslation },
vi: { translation: viTranslation },
ko: { translation: koTranslation },
lo: { translation: loTranslation },
it: { translation: itTranslation },
},
lng: 'th',
fallbackLng: 'en',
+1 -1
View File
@@ -1,4 +1,4 @@
export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const;
export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'] as const;
export type Locale = typeof SUPPORTED_LOCALES[number];
export const DEFAULT_LOCALE: Locale = 'th';
@@ -0,0 +1,68 @@
{
"nav": {
"home": "Home",
"shop": "Negozio",
"promotions": "Promozioni",
"collections": "Collezioni",
"scentGuide": "Guida alle fragranze",
"about": "Chi siamo",
"menu": "Menu",
"faq": "FAQ",
"contact": "Contatti",
"shopNow": "Acquista ora",
"findMyScent": "Trova la tua fragranza ✨",
"search": {
"title": "Cerca nella collezione",
"placeholder": "Cerca fragranze, categorie...",
"quickLinks": "Link rapidi",
"allProducts": "Tutti i prodotti"
}
},
"cart": {
"title": "Il tuo carrello",
"items": "{{count}} articoli",
"empty": "Il tuo carrello è vuoto.",
"continueShopping": "Continua lo shopping",
"subtotal": "Subtotale",
"shippingNote": "Spedizione e tasse saranno calcolate al checkout.",
"proceed": "Vai al checkout"
},
"footer": {
"catchphrase": "Eleva il tuo spazio con fragranze premium.",
"explore": "Esplora",
"support": "Supporto",
"followUs": "Seguici",
"contactUs": "Contattaci",
"rights": "Tutti i diritti riservati."
},
"dynamic": {
"heroLearnMore": "Scopri di più",
"products": "Prodotti",
"viewAll": "Vedi tutta la collezione →",
"emptyProducts": "Nessun prodotto trovato in questa sezione.",
"authorBy": "Di",
"notes": {
"top": "Note di testa",
"heart": "Note di cuore",
"base": "Note di fondo"
},
"contactInfo": {
"address": "Indirizzo",
"email": "Email",
"line": "Line",
"hours": "Orari"
}
},
"contactForm": {
"title": "Contattaci",
"subtitle": "Invia un messaggio al nostro team e ti risponderemo presto.",
"successTitle": "Messaggio inviato con successo",
"successBody": "Grazie per averci contattato. Il nostro team risponderà il prima possibile.",
"name": "Nome completo",
"namePlaceholder": "Inserisci il tuo nome",
"email": "Email",
"message": "Messaggio",
"messagePlaceholder": "Scrivi qui il tuo messaggio...",
"submit": "Invia messaggio"
}
}
@@ -0,0 +1,68 @@
{
"nav": {
"home": "ໜ້າຫຼັກ",
"shop": "ຮ້ານຄ້າ",
"promotions": "ໂປຣໂມຊັນ",
"collections": "ຄໍເລັກຊັນ",
"scentGuide": "ຄູ່ມືກິ່ນຫອມ",
"about": "ກ່ຽວກັບພວກເຮົາ",
"menu": "ເມນູ",
"faq": "ຄຳຖາມທີ່ພົບເລື້ອຍ",
"contact": "ຕິດຕໍ່",
"shopNow": "ຊື້ຕອນນີ້",
"findMyScent": "ຄົ້ນຫາກິ່ນທີ່ເໝາະກັບທ່ານ ✨",
"search": {
"title": "ຄົ້ນຫາຄໍເລັກຊັນ",
"placeholder": "ຄົ້ນຫາກິ່ນ, ໝວດສິນຄ້າ...",
"quickLinks": "ລິ້ງດ່ວນ",
"allProducts": "ສິນຄ້າທັງໝົດ"
}
},
"cart": {
"title": "ຕະກ້າສິນຄ້າຂອງທ່ານ",
"items": "{{count}} ລາຍການ",
"empty": "ຕະກ້າສິນຄ້າຂອງທ່ານວ່າງຢູ່.",
"continueShopping": "ຊື້ສິນຄ້າຕໍ່",
"subtotal": "ຍອດລວມ",
"shippingNote": "ຄ່າຈັດສົ່ງ ແລະ ພາສີຈະຄຳນວນໃນຂັ້ນຕອນຊຳລະເງິນ.",
"proceed": "ໄປຊຳລະເງິນ"
},
"footer": {
"catchphrase": "ຍົກລະດັບພື້ນທີ່ຂອງທ່ານດ້ວຍກິ່ນຫອມພຣີມຽມ.",
"explore": "ສຳຫຼວດ",
"support": "ຊ່ວຍເຫຼືອ",
"followUs": "ຕິດຕາມພວກເຮົາ",
"contactUs": "ຕິດຕໍ່ພວກເຮົາ",
"rights": "ສະຫງວນລິຂະສິດ."
},
"dynamic": {
"heroLearnMore": "ຮຽນຮູ້ເພີ່ມເຕີມ",
"products": "ສິນຄ້າ",
"viewAll": "ເບິ່ງຄໍເລັກຊັນທັງໝົດ →",
"emptyProducts": "ບໍ່ພົບສິນຄ້າໃນສ່ວນນີ້.",
"authorBy": "ໂດຍ",
"notes": {
"top": "ກິ່ນຊັ້ນຕົ້ນ",
"heart": "ກິ່ນຊັ້ນກາງ",
"base": "ກິ່ນຊັ້ນຖານ"
},
"contactInfo": {
"address": "ທີ່ຢູ່",
"email": "ອີເມວ",
"line": "Line",
"hours": "ເວລາເຮັດການ"
}
},
"contactForm": {
"title": "ຕິດຕໍ່ພວກເຮົາ",
"subtitle": "ສົ່ງຂໍ້ຄວາມຫາທີມງານຂອງພວກເຮົາ ແລະ ພວກເຮົາຈະຕອບກັບໂດຍໄວ.",
"successTitle": "ສົ່ງຂໍ້ຄວາມສຳເລັດ",
"successBody": "ຂອບໃຈທີ່ຕິດຕໍ່ມາ. ທີມງານຈະຕອບກັບໂດຍໄວທີ່ສຸດ.",
"name": "ຊື່ເຕັມ",
"namePlaceholder": "ກອກຊື່ຂອງທ່ານ",
"email": "ອີເມວ",
"message": "ຂໍ້ຄວາມ",
"messagePlaceholder": "ພິມຂໍ້ຄວາມຂອງທ່ານທີ່ນີ້...",
"submit": "ສົ່ງຂໍ້ຄວາມ"
}
}