diff --git a/apps/website/src/app/[slug]/page.tsx b/apps/website/src/app/[lang]/[slug]/page.tsx similarity index 88% rename from apps/website/src/app/[slug]/page.tsx rename to apps/website/src/app/[lang]/[slug]/page.tsx index e5a06a1..f65f8d7 100644 --- a/apps/website/src/app/[slug]/page.tsx +++ b/apps/website/src/app/[lang]/[slug]/page.tsx @@ -1,12 +1,11 @@ import { notFound } from 'next/navigation' import type { Metadata } from 'next' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params const reservedSlugs = new Set([ 'about', 'article', @@ -32,8 +31,8 @@ function isReservedSlug(slug: string) { return reservedSlugs.has(normalizeSlug(slug)) } -export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { - const { slug } = await params +export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise { + const { slug, lang } = await params if (isReservedSlug(slug)) return { title: 'Page Not Found | HomAroma' } const pageResult = await getCmsPageResult(slug, lang) @@ -44,8 +43,8 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str } } -export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params +export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string, lang: string }> }) { + const { slug, lang } = await params if (isReservedSlug(slug)) notFound() const [pageResult, categoriesResult] = await Promise.all([ diff --git a/apps/website/src/app/about/page.tsx b/apps/website/src/app/[lang]/about/page.tsx similarity index 99% rename from apps/website/src/app/about/page.tsx rename to apps/website/src/app/[lang]/about/page.tsx index 3cec1cc..84e74c0 100644 --- a/apps/website/src/app/about/page.tsx +++ b/apps/website/src/app/[lang]/about/page.tsx @@ -1,4 +1,4 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' const PILLARS = [ { diff --git a/apps/website/src/app/article/[slug]/page.tsx b/apps/website/src/app/[lang]/article/[slug]/page.tsx similarity index 97% rename from apps/website/src/app/article/[slug]/page.tsx rename to apps/website/src/app/[lang]/article/[slug]/page.tsx index 5ac189a..97c3015 100644 --- a/apps/website/src/app/article/[slug]/page.tsx +++ b/apps/website/src/app/[lang]/article/[slug]/page.tsx @@ -1,10 +1,10 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' import { notFound } from 'next/navigation' import type { Metadata } from 'next' import { ArrowLeft } from 'lucide-react' import type { Article } from '@homaroma/types' -import { API_BASE } from '../../../lib/api' +import { API_BASE } from '@/lib/api' async function getArticle(slug: string): Promise
{ try { diff --git a/apps/website/src/app/category/[id]/page.tsx b/apps/website/src/app/[lang]/category/[id]/page.tsx similarity index 88% rename from apps/website/src/app/category/[id]/page.tsx rename to apps/website/src/app/[lang]/category/[id]/page.tsx index 2167b09..d0dd5c1 100644 --- a/apps/website/src/app/category/[id]/page.tsx +++ b/apps/website/src/app/[lang]/category/[id]/page.tsx @@ -1,30 +1,29 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' import { notFound } from 'next/navigation' import type { Metadata } from 'next' import type { Category, Product } from '@homaroma/types' import { getCategoryImage } from '@homaroma/types' -import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer' -import ProductCard from '../../../components/ProductCard' -import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult } from '../../../lib/cms' -import { splitCmsLayoutSections } from '../../../lib/cms-sections' -import { resolveLocalized } from '../../../lib/localization' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import ProductCard from '@/components/ProductCard' +import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +import { resolveLocalized } from '@/lib/localization' +// lang is extracted from params type CategoryProductsResponse = { category?: Category data?: Product[] } -async function getCategoryProducts(id: string): Promise> { +async function getCategoryProducts(id: string, lang: string): Promise> { return await fetchJsonResult(`/categories/${id}/products?limit=1000&lang=${lang}`, { next: { revalidate: 60 }, }) } -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { - const { id } = await params - const result = await getCategoryProducts(id) +export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise { + const { id, lang } = await params + const result = await getCategoryProducts(id, lang) const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : '' return { @@ -33,10 +32,10 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin } } -export default async function CategoryPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params +export default async function CategoryPage({ params }: { params: Promise<{ id: string, lang: string }> }) { + const { id, lang } = await params const [categoryProductsResult, categoriesResult, cmsLayoutResult] = await Promise.all([ - getCategoryProducts(id), + getCategoryProducts(id, lang), getCategoriesResult(lang), getCmsPageResult('category-layout', lang), ]) diff --git a/apps/website/src/app/checkout/page.tsx b/apps/website/src/app/[lang]/checkout/page.tsx similarity index 85% rename from apps/website/src/app/checkout/page.tsx rename to apps/website/src/app/[lang]/checkout/page.tsx index 0fbcc6c..4e14d5e 100644 --- a/apps/website/src/app/checkout/page.tsx +++ b/apps/website/src/app/[lang]/checkout/page.tsx @@ -1,18 +1,18 @@ import { CreditCard } from 'lucide-react' -import CheckoutClient from '../../components/CheckoutClient' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import CheckoutClient from '@/components/CheckoutClient' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params export const metadata = { title: 'Checkout | HomAroma', description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ HomAroma', } -export default async function CheckoutPage() { +export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [categoriesResult, cmsLayoutResult] = await Promise.all([ getCategoriesResult(lang), getCmsPageResult('checkout-layout', lang), diff --git a/apps/website/src/app/contact/layout.tsx b/apps/website/src/app/[lang]/contact/layout.tsx similarity index 100% rename from apps/website/src/app/contact/layout.tsx rename to apps/website/src/app/[lang]/contact/layout.tsx diff --git a/apps/website/src/app/contact/page.tsx b/apps/website/src/app/[lang]/contact/page.tsx similarity index 100% rename from apps/website/src/app/contact/page.tsx rename to apps/website/src/app/[lang]/contact/page.tsx diff --git a/apps/website/src/app/coupons/page.tsx b/apps/website/src/app/[lang]/coupons/page.tsx similarity index 94% rename from apps/website/src/app/coupons/page.tsx rename to apps/website/src/app/[lang]/coupons/page.tsx index 30c1023..1714c05 100644 --- a/apps/website/src/app/coupons/page.tsx +++ b/apps/website/src/app/[lang]/coupons/page.tsx @@ -1,11 +1,10 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' import { Ticket, ArrowRight } from 'lucide-react' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params function formatDiscount(type: string, value: number) { if (type === 'PERCENTAGE') return `${value}% OFF` if (type === 'FIXED_AMOUNT') return `ลด ${value} ฿` @@ -18,7 +17,8 @@ export const metadata = { description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ', } -export default async function CouponsPage() { +export default async function CouponsPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [couponsResult, categoriesResult, cmsLayoutResult] = await Promise.all([ getActiveCouponsResult(), getCategoriesResult(lang), diff --git a/apps/website/src/app/faq/layout.tsx b/apps/website/src/app/[lang]/faq/layout.tsx similarity index 100% rename from apps/website/src/app/faq/layout.tsx rename to apps/website/src/app/[lang]/faq/layout.tsx diff --git a/apps/website/src/app/faq/page.tsx b/apps/website/src/app/[lang]/faq/page.tsx similarity index 99% rename from apps/website/src/app/faq/page.tsx rename to apps/website/src/app/[lang]/faq/page.tsx index 8891b1b..3577434 100644 --- a/apps/website/src/app/faq/page.tsx +++ b/apps/website/src/app/[lang]/faq/page.tsx @@ -1,6 +1,6 @@ "use client"; import { useState } from 'react' -import Link from 'next/link' +import Link from '@/components/LocalizedLink' const FAQS = [ { diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/[lang]/layout.tsx similarity index 62% rename from apps/website/src/app/layout.tsx rename to apps/website/src/app/[lang]/layout.tsx index cdadb5d..066cffb 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/[lang]/layout.tsx @@ -1,21 +1,22 @@ import type { Metadata } from 'next'; -import './globals.css'; -import Navbar from '../components/Navbar'; -import Footer from '../components/Footer'; -import ClientProviders from '../components/ClientProviders'; -import { API_BASE } from '../lib/api'; -import type { MegaMenuPayload } from '../components/navigation/menuTypes'; +import '../globals.css'; +import Navbar from '@/components/Navbar'; +import Footer from '@/components/Footer'; +import ClientProviders from '@/components/ClientProviders'; +import { API_BASE } from '@/lib/api'; +import type { MegaMenuPayload } from '@/components/navigation/menuTypes'; import type { MenuItem, StoreSettings } from '@homaroma/types'; export const metadata: Metadata = { title: 'HomAroma', description: 'Pure Origins, Artisan Craft, Zero Compromise', + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), }; -// Fetch categories for Navbar and Footer on the server (lang=all for client-side switching) -async function getCategories() { +// Fetch categories for Navbar and Footer on the server +async function getCategories(lang: string) { try { - const res = await fetch(`${API_BASE}/categories?lang=all`, { next: { revalidate: 3600 } }); + const res = await fetch(`${API_BASE}/categories?lang=${lang}`, { next: { revalidate: 3600 } }); if (!res.ok) return []; return await res.json(); } catch (error) { @@ -25,9 +26,9 @@ async function getCategories() { } // Fetch mega menu on the server — eliminates client-side flash -async function getMegaMenu(): Promise { +async function getMegaMenu(lang: string): Promise { try { - const res = await fetch(`${API_BASE}/menus/main`, { next: { revalidate: 300 } }); + const res = await fetch(`${API_BASE}/menus/main?lang=${lang}`, { next: { revalidate: 300 } }); if (!res.ok) return []; const data = await res.json() as { payload?: MegaMenuPayload[] }; return Array.isArray(data.payload) ? data.payload : []; @@ -38,10 +39,10 @@ async function getMegaMenu(): Promise { } // Fetch footer menus (explore + support) on the server -async function getFooterMenus(): Promise<{ exploreMenu: MenuItem[]; supportMenu: MenuItem[] }> { +async function getFooterMenus(lang: string): Promise<{ exploreMenu: MenuItem[]; supportMenu: MenuItem[] }> { const fetchMenu = async (slug: string): Promise => { try { - const res = await fetch(`${API_BASE}/menus/${slug}`, { next: { revalidate: 300 } }); + const res = await fetch(`${API_BASE}/menus/${slug}?lang=${lang}`, { next: { revalidate: 300 } }); if (!res.ok) return []; const data = await res.json() as { items?: MenuItem[] }; return Array.isArray(data.items) @@ -61,9 +62,9 @@ async function getFooterMenus(): Promise<{ exploreMenu: MenuItem[]; supportMenu: } // Fetch store settings (social links, footer texts) on the server -async function getStoreSettings(): Promise { +async function getStoreSettings(lang: string): Promise { try { - const res = await fetch(`${API_BASE}/settings`, { next: { revalidate: 600 } }); + const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: 600 } }); if (!res.ok) return null; return await res.json() as StoreSettings; } catch (error) { @@ -74,18 +75,22 @@ async function getStoreSettings(): Promise { export default async function RootLayout({ children, + params, }: { children: React.ReactNode; + params: Promise<{ lang: string }>; }) { + const { lang } = await params; + const [categories, initialMenu, footerMenus, settings] = await Promise.all([ - getCategories(), - getMegaMenu(), - getFooterMenus(), - getStoreSettings(), + getCategories(lang), + getMegaMenu(lang), + getFooterMenus(lang), + getStoreSettings(lang), ]); return ( - + diff --git a/apps/website/src/app/order/[id]/page.tsx b/apps/website/src/app/[lang]/order/[id]/page.tsx similarity index 96% rename from apps/website/src/app/order/[id]/page.tsx rename to apps/website/src/app/[lang]/order/[id]/page.tsx index dbab459..c9a0ae2 100644 --- a/apps/website/src/app/order/[id]/page.tsx +++ b/apps/website/src/app/[lang]/order/[id]/page.tsx @@ -1,15 +1,14 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' import { notFound } from 'next/navigation' import type { Metadata } from 'next' import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react' import { formatPrice } from '@homaroma/types' -import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer' -import { getCategoriesResult, getCmsPageResult, getOrderResult } from '../../../lib/cms' -import { splitCmsLayoutSections } from '../../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getCategoriesResult, getCmsPageResult, getOrderResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params function orderLabel(id: number) { return `#${id.toString().padStart(4, '0')}` } @@ -47,8 +46,8 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin } } -export default async function OrderStatusPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params +export default async function OrderStatusPage({ params }: { params: Promise<{ id: string, lang: string }> }) { + const { id, lang } = await params const [orderResult, categoriesResult, cmsLayoutResult] = await Promise.all([ getOrderResult(id), getCategoriesResult(lang), diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/[lang]/page.tsx similarity index 79% rename from apps/website/src/app/page.tsx rename to apps/website/src/app/[lang]/page.tsx index 544b15a..9bd3096 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/[lang]/page.tsx @@ -1,11 +1,14 @@ import type { Metadata } from 'next' -import DynamicPageRenderer from '../components/dynamic/DynamicPageRenderer' -import { getCategories, getCmsPage } from '../lib/cms' -import { splitCmsLayoutSections } from '../lib/cms-sections' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getCategories, getCmsPage } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' -const lang = 'all' +type Props = { + params: Promise<{ lang: string }> +} -export async function generateMetadata(): Promise { +export async function generateMetadata({ params }: Props): Promise { + const { lang } = await params const page = await getCmsPage('home', lang) return { @@ -14,7 +17,9 @@ export async function generateMetadata(): Promise { } } -export default async function HomePage() { +export default async function HomePage({ params }: Props) { + const { lang } = await params + const [categories, page] = await Promise.all([ getCategories(lang), getCmsPage('home', lang), diff --git a/apps/website/src/app/products/[id]/page.tsx b/apps/website/src/app/[lang]/products/[id]/page.tsx similarity index 89% rename from apps/website/src/app/products/[id]/page.tsx rename to apps/website/src/app/[lang]/products/[id]/page.tsx index d7fe926..8120e1d 100644 --- a/apps/website/src/app/products/[id]/page.tsx +++ b/apps/website/src/app/[lang]/products/[id]/page.tsx @@ -1,24 +1,22 @@ -import Link from 'next/link' +import Link from '@/components/LocalizedLink' import { notFound } from 'next/navigation' import type { Metadata } from 'next' import { getCategoryImage, type Product } from '@homaroma/types' -import AddToCartSection from '../../../components/AddToCartSection' -import DynamicPageRenderer from '../../../components/dynamic/DynamicPageRenderer' -import ProductCard from '../../../components/ProductCard' -import { getCategoriesResult, getCmsPageResult, getProduct, getProductResult, getProductsResult } from '../../../lib/cms' -import { splitCmsLayoutSections } from '../../../lib/cms-sections' -import { resolveCategoryName, resolveLocalized } from '../../../lib/localization' +import AddToCartSection from '@/components/AddToCartSection' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import ProductCard from '@/components/ProductCard' +import { getCategoriesResult, getCmsPageResult, getProduct, getProductResult, getProductsResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +import { resolveCategoryName, resolveLocalized } from '@/lib/localization' -const lang = 'all' - -async function getRelated(categoryId: number, excludeId: number): Promise { +async function getRelated(categoryId: number, excludeId: number, lang: string): Promise { const result = await getProductsResult(`categoryId=${categoryId}`, lang) if (!result.ok) return [] return result.data.filter((product) => product.id !== excludeId).slice(0, 6) } -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { - const { id } = await params +export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise { + const { id, lang } = await params const product = await getProduct(id, lang) if (!product) return { title: 'Product Not Found | HomAroma' } @@ -38,8 +36,8 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin } } -export default async function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params +export default async function ProductDetailPage({ params }: { params: Promise<{ id: string, lang: string }> }) { + const { id, lang } = await params const [productResult, categoriesResult, cmsLayoutResult] = await Promise.all([ getProductResult(id, lang), getCategoriesResult(lang), @@ -73,7 +71,7 @@ export default async function ProductDetailPage({ params }: { params: Promise<{ } const product = productResult.data - const related = await getRelated(product.category.id, product.id) + const related = await getRelated(product.category.id, product.id, lang) const productName = resolveLocalized(product.name, lang) || product.scent.name const categoryName = resolveCategoryName(product.category, lang) const details = resolveLocalized(product.details, lang) diff --git a/apps/website/src/app/profile/page.tsx b/apps/website/src/app/[lang]/profile/page.tsx similarity index 85% rename from apps/website/src/app/profile/page.tsx rename to apps/website/src/app/[lang]/profile/page.tsx index 65de0be..71838ef 100644 --- a/apps/website/src/app/profile/page.tsx +++ b/apps/website/src/app/[lang]/profile/page.tsx @@ -1,18 +1,18 @@ import { User } from 'lucide-react' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import ProfileClient from '../../components/ProfileClient' -import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import ProfileClient from '@/components/ProfileClient' +import { getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params export const metadata = { title: 'โปรไฟล์สมาชิก | HomAroma', description: 'จัดการโปรไฟล์สมาชิก HomAroma ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง', } -export default async function ProfilePage() { +export default async function ProfilePage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [categoriesResult, cmsLayoutResult] = await Promise.all([ getCategoriesResult(lang), getCmsPageResult('profile-layout', lang), diff --git a/apps/website/src/app/scent-guide/page.tsx b/apps/website/src/app/[lang]/scent-guide/page.tsx similarity index 91% rename from apps/website/src/app/scent-guide/page.tsx rename to apps/website/src/app/[lang]/scent-guide/page.tsx index aaafae8..266dd98 100644 --- a/apps/website/src/app/scent-guide/page.tsx +++ b/apps/website/src/app/[lang]/scent-guide/page.tsx @@ -1,16 +1,16 @@ -import Link from 'next/link' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import Link from '@/components/LocalizedLink' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params export const metadata = { title: 'Scent Guide | HomAroma', description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.', } -export default async function ScentGuidePage() { +export default async function ScentGuidePage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [articlesResult, categoriesResult, cmsLayoutResult] = await Promise.all([ getArticlesResult(), getCategoriesResult(lang), diff --git a/apps/website/src/app/search/layout.tsx b/apps/website/src/app/[lang]/search/layout.tsx similarity index 100% rename from apps/website/src/app/search/layout.tsx rename to apps/website/src/app/[lang]/search/layout.tsx diff --git a/apps/website/src/app/search/page.tsx b/apps/website/src/app/[lang]/search/page.tsx similarity index 89% rename from apps/website/src/app/search/page.tsx rename to apps/website/src/app/[lang]/search/page.tsx index c9d60b6..78dadff 100644 --- a/apps/website/src/app/search/page.tsx +++ b/apps/website/src/app/[lang]/search/page.tsx @@ -1,20 +1,19 @@ -import Link from 'next/link' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import ProductCard from '../../components/ProductCard' -import { getCategoriesResult, getCmsPageResult, getProductsResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' -import { resolveLocalized } from '../../lib/localization' - -const lang = 'all' +import Link from '@/components/LocalizedLink' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import ProductCard from '@/components/ProductCard' +import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +import { resolveLocalized } from '@/lib/localization' export const metadata = { title: 'Search Products | HomAroma', description: 'Search HomAroma products by scent, category, or keyword.', } -export default async function SearchPage({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) { - const params = await searchParams - const rawQuery = Array.isArray(params.q) ? params.q[0] : params.q +export default async function SearchPage({ searchParams, params }: { searchParams: Promise<{ q?: string | string[] }>, params: Promise<{ lang: string }> }) { + const { lang } = await params + const sParams = await searchParams + const rawQuery = Array.isArray(sParams.q) ? sParams.q[0] : sParams.q const query = rawQuery?.trim() ?? '' const [categoriesResult, cmsLayoutResult, productsResult] = await Promise.all([ getCategoriesResult(lang), diff --git a/apps/website/src/app/shop/layout.tsx b/apps/website/src/app/[lang]/shop/layout.tsx similarity index 100% rename from apps/website/src/app/shop/layout.tsx rename to apps/website/src/app/[lang]/shop/layout.tsx diff --git a/apps/website/src/app/shop/page.tsx b/apps/website/src/app/[lang]/shop/page.tsx similarity index 85% rename from apps/website/src/app/shop/page.tsx rename to apps/website/src/app/[lang]/shop/page.tsx index 0c035d1..f85a243 100644 --- a/apps/website/src/app/shop/page.tsx +++ b/apps/website/src/app/[lang]/shop/page.tsx @@ -1,21 +1,21 @@ -import Link from 'next/link' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import ProductCard from '../../components/ProductCard' -import ShopCouponBridge from '../../components/ShopCouponBridge' -import { getCategoriesResult, getCmsPageResult, getProductsResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' -import { resolveLocalized } from '../../lib/localization' - -const lang = 'all' +import Link from '@/components/LocalizedLink' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import ProductCard from '@/components/ProductCard' +import ShopCouponBridge from '@/components/ShopCouponBridge' +import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +import { resolveLocalized } from '@/lib/localization' +// lang is extracted from params export const metadata = { title: 'Shop Aroma Products | HomAroma', description: 'Browse HomAroma aroma oils, reed diffusers, room sprays, and premium scent products.', } -export default async function ShopPage({ searchParams }: { searchParams: Promise<{ coupon?: string | string[] }> }) { - const params = await searchParams - const coupon = Array.isArray(params.coupon) ? params.coupon[0] : params.coupon +export default async function ShopPage({ searchParams, params }: { searchParams: Promise<{ coupon?: string | string[] }>, params: Promise<{ lang: string }> }) { + const { lang } = await params + const sParams = await searchParams + const coupon = Array.isArray(sParams.coupon) ? sParams.coupon[0] : sParams.coupon const [categoriesResult, productsResult, cmsLayoutResult] = await Promise.all([ getCategoriesResult(lang), getProductsResult('limit=1000', lang), diff --git a/apps/website/src/app/track/page.tsx b/apps/website/src/app/[lang]/track/page.tsx similarity index 85% rename from apps/website/src/app/track/page.tsx rename to apps/website/src/app/[lang]/track/page.tsx index 4cf8e4a..42b5e54 100644 --- a/apps/website/src/app/track/page.tsx +++ b/apps/website/src/app/[lang]/track/page.tsx @@ -1,18 +1,18 @@ import { MapPin } from 'lucide-react' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import TrackOrderClient from '../../components/TrackOrderClient' -import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import TrackOrderClient from '@/components/TrackOrderClient' +import { getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params export const metadata = { title: 'ติดตามคำสั่งซื้อ | HomAroma', description: 'ตรวจสอบสถานะคำสั่งซื้อ HomAroma ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ', } -export default async function TrackPage() { +export default async function TrackPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [categoriesResult, cmsLayoutResult] = await Promise.all([ getCategoriesResult(lang), getCmsPageResult('track-order-layout', lang), diff --git a/apps/website/src/app/wishlist/page.tsx b/apps/website/src/app/[lang]/wishlist/page.tsx similarity index 83% rename from apps/website/src/app/wishlist/page.tsx rename to apps/website/src/app/[lang]/wishlist/page.tsx index 5d5f11d..a768355 100644 --- a/apps/website/src/app/wishlist/page.tsx +++ b/apps/website/src/app/[lang]/wishlist/page.tsx @@ -1,18 +1,18 @@ import { Heart } from 'lucide-react' -import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' -import WishlistClient from '../../components/WishlistClient' -import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' -import { splitCmsLayoutSections } from '../../lib/cms-sections' - -const lang = 'all' +import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer' +import WishlistClient from '@/components/WishlistClient' +import { getCategoriesResult, getCmsPageResult } from '@/lib/cms' +import { splitCmsLayoutSections } from '@/lib/cms-sections' +// lang is extracted from params export const metadata = { title: 'Wishlist | HomAroma', description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ HomAroma', } -export default async function WishlistPage() { +export default async function WishlistPage({ params }: { params: Promise<{ lang: string }> }) { + const { lang } = await params const [categoriesResult, cmsLayoutResult] = await Promise.all([ getCategoriesResult(lang), getCmsPageResult('wishlist-layout', lang), diff --git a/apps/website/src/app/robots.ts b/apps/website/src/app/robots.ts new file mode 100644 index 0000000..b55ec65 --- /dev/null +++ b/apps/website/src/app/robots.ts @@ -0,0 +1,14 @@ +import { MetadataRoute } from 'next'; + +const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/_next/', '/profile/', '/checkout/', '/order/'], + }, + sitemap: `${baseUrl}/sitemap.xml`, + }; +} diff --git a/apps/website/src/app/sitemap.ts b/apps/website/src/app/sitemap.ts new file mode 100644 index 0000000..b5d8912 --- /dev/null +++ b/apps/website/src/app/sitemap.ts @@ -0,0 +1,78 @@ +import { MetadataRoute } from 'next'; +import { getCategories, getProducts, getArticlesResult } from '../lib/cms'; + +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 = {}; + for (const lang of languages) { + alternates[lang] = `${baseUrl}/${lang}${path}`; + } + return alternates; +} + +export default async function sitemap(): Promise { + const [categories, products, articlesResult] = await Promise.all([ + getCategories(), + getProducts('limit=1000'), + getArticlesResult(), + ]); + + const staticPages = [ + '', + '/shop', + '/scent-guide', + '/about', + '/contact', + '/faq', + ]; + + const routes: MetadataRoute.Sitemap = staticPages.map((route) => ({ + url: `${baseUrl}/th${route}`, + lastModified: new Date(), + changeFrequency: 'daily', + priority: route === '' ? 1 : 0.8, + alternates: { + languages: createAlternates(route), + }, + })); + + const categoryRoutes: MetadataRoute.Sitemap = categories.map((category) => ({ + url: `${baseUrl}/th/category/${category.id}`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.7, + alternates: { + languages: createAlternates(`/category/${category.id}`), + }, + })); + + const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({ + url: `${baseUrl}/th/products/${product.id}`, + lastModified: new Date(), + changeFrequency: 'daily', + priority: 0.9, + alternates: { + languages: createAlternates(`/products/${product.id}`), + }, + })); + + const articleRoutes: MetadataRoute.Sitemap = []; + if (articlesResult.ok && articlesResult.data) { + articlesResult.data.forEach((article) => { + articleRoutes.push({ + url: `${baseUrl}/th/article/${article.slug}`, + lastModified: new Date(article.updatedAt || article.createdAt), + changeFrequency: 'monthly', + priority: 0.6, + alternates: { + languages: createAlternates(`/article/${article.slug}`), + }, + }); + }); + } + + return [...routes, ...categoryRoutes, ...productRoutes, ...articleRoutes]; +} diff --git a/apps/website/src/components/ClientProviders.tsx b/apps/website/src/components/ClientProviders.tsx index f03e350..0f2df7b 100644 --- a/apps/website/src/components/ClientProviders.tsx +++ b/apps/website/src/components/ClientProviders.tsx @@ -7,11 +7,13 @@ import i18n from '../lib/i18n'; export default function ClientProviders({ children }: { children: ReactNode }) { useEffect(() => { - const savedLang = localStorage.getItem('homaroma_lang') - - // Hydrate from the user's persisted language only after the first client render. - if (savedLang && savedLang !== i18n.language) { - void i18n.changeLanguage(savedLang) + // 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'; + + if (targetLang !== i18n.language) { + void i18n.changeLanguage(targetLang); } }, []) diff --git a/apps/website/src/components/Footer.tsx b/apps/website/src/components/Footer.tsx index a936367..c92323a 100644 --- a/apps/website/src/components/Footer.tsx +++ b/apps/website/src/components/Footer.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from 'react' -import Link from 'next/link' +import Link from './LocalizedLink' import type { Category, MenuItem, StoreSettings } from '@homaroma/types' import { useTranslation } from 'react-i18next' import { resolveLocalized } from '../lib/localization' diff --git a/apps/website/src/components/LocalizedLink.tsx b/apps/website/src/components/LocalizedLink.tsx new file mode 100644 index 0000000..84cd9d7 --- /dev/null +++ b/apps/website/src/components/LocalizedLink.tsx @@ -0,0 +1,63 @@ +'use client'; + +import Link, { LinkProps } from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useMemo } from 'react'; +import { getPathWithLocale, SUPPORTED_LOCALES } from '../lib/locales'; + +interface LocalizedLinkProps extends Omit, keyof LinkProps>, LinkProps { + children: React.ReactNode; + activeClassName?: string; + exact?: boolean; +} + +export default function LocalizedLink({ + href, + children, + className = '', + activeClassName = '', + exact = false, + ...props +}: LocalizedLinkProps) { + const pathname = usePathname(); + + // Extract current locale from pathname + const currentLocale = useMemo(() => { + const segments = pathname.split('/').filter(Boolean); + if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) { + return segments[0]; + } + return 'th'; // Fallback + }, [pathname]); + + // Resolve the localized href + const localizedHref = useMemo(() => { + if (typeof href === 'string') { + return getPathWithLocale(href, currentLocale); + } + if (typeof href === 'object' && href.pathname) { + return { + ...href, + pathname: getPathWithLocale(href.pathname, currentLocale) + }; + } + return href; + }, [href, currentLocale]); + + // Check if link is active + const isActive = useMemo(() => { + const targetPath = typeof localizedHref === 'string' ? localizedHref : localizedHref.pathname || ''; + if (exact) { + return pathname === targetPath; + } + return pathname.startsWith(targetPath) && targetPath !== `/${currentLocale}`; + }, [pathname, localizedHref, exact, currentLocale]); + + const combinedClassName = `${className} ${isActive ? activeClassName : ''}`.trim(); + + return ( + + {children} + + ); +} diff --git a/apps/website/src/components/Navbar.tsx b/apps/website/src/components/Navbar.tsx index 44e7653..42c3775 100644 --- a/apps/website/src/components/Navbar.tsx +++ b/apps/website/src/components/Navbar.tsx @@ -1,6 +1,7 @@ "use client" -import Link from 'next/link' +import Link from './LocalizedLink' +import { getPathWithoutLocale, getPathWithLocale } from '../lib/locales' import { useRouter, usePathname } from 'next/navigation' import { useState, useEffect, useMemo, useRef } from 'react' import type { Category } from '@homaroma/types' @@ -89,7 +90,13 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n const changeLanguage = (code: string) => { i18n.changeLanguage(code) localStorage.setItem('homaroma_lang', code) + document.cookie = `homaroma_lang=${code}; path=/; max-age=31536000` + + const currentPathWithoutLang = getPathWithoutLocale(pathname) + const newPath = getPathWithLocale(currentPathWithoutLang, code) + setLangSheetOpen(false) + router.push(newPath) } const loginWithLine = () => { diff --git a/apps/website/src/components/navigation/DesktopMegaMenu.tsx b/apps/website/src/components/navigation/DesktopMegaMenu.tsx index 5a2e59d..3e6585a 100644 --- a/apps/website/src/components/navigation/DesktopMegaMenu.tsx +++ b/apps/website/src/components/navigation/DesktopMegaMenu.tsx @@ -1,6 +1,6 @@ "use client" -import Link from 'next/link' +import Link from '../LocalizedLink' import { useState, useRef } from 'react' import type { MegaMenuPayload } from './menuTypes' import { getLocalizedMenuText } from './menuTypes' diff --git a/apps/website/src/components/navigation/MobileDrawer.tsx b/apps/website/src/components/navigation/MobileDrawer.tsx index c8bcbe3..2c8728e 100644 --- a/apps/website/src/components/navigation/MobileDrawer.tsx +++ b/apps/website/src/components/navigation/MobileDrawer.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from 'react' -import Link from 'next/link' +import Link from '../LocalizedLink' import { ChevronLeft, ChevronRight, Globe, X } from 'lucide-react' import type { MegaMenuPayload, MenuColumn, MenuLink } from './menuTypes' import { getLocalizedMenuText } from './menuTypes' diff --git a/apps/website/src/hooks/useLang.ts b/apps/website/src/hooks/useLang.ts index 350baaf..b3e18e5 100644 --- a/apps/website/src/hooks/useLang.ts +++ b/apps/website/src/hooks/useLang.ts @@ -1,34 +1,32 @@ -import { useState, useEffect } from 'react' -import i18n from '../lib/i18n' +import { useParams, usePathname } from 'next/navigation'; +import { useMemo } from 'react'; +import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../lib/locales'; /** - * Returns a hydration-safe 2-char language code. - * - * WHY this pattern is necessary: - * - i18n.ts reads localStorage synchronously at module-load time (before React hydrates) - * - So i18n.language may already be 'ja', 'ko', etc. when client components first render - * - useTranslation() subscribes to this and causes components to render the user's language - * DURING hydration, but the server rendered 'th' — causing a mismatch error - * - * SOLUTION: - * - Always return 'th' until the component has mounted (matching the server) - * - After mount, sync to the real language and subscribe to future changes - * - Components using this hook NEVER cause a hydration mismatch + * Returns the current active language based on the URL. + * With Locale Routing, the language is dictated by the first path segment + * (e.g., /en/shop -> 'en'). */ export function useLang(): string { - // Always 'th' on first render (matches SSR) — updated post-hydration - const [lang, setLang] = useState('th') - - useEffect(() => { - // Sync immediately after hydration - const current = i18n.language ? i18n.language.substring(0, 2) : 'th' - setLang(current) - - // Subscribe to future language changes (user clicks language button) - const handler = (lng: string) => setLang(lng.substring(0, 2)) - i18n.on('languageChanged', handler) - return () => { i18n.off('languageChanged', handler) } - }, []) - - return lang + const params = useParams(); + const pathname = usePathname(); + + return useMemo(() => { + // 1. Try to get it from route params + if (params?.lang && typeof params.lang === 'string') { + if (SUPPORTED_LOCALES.includes(params.lang as any)) { + return params.lang; + } + } + + // 2. Fallback: extract from pathname if params are not available yet + if (pathname) { + const segments = pathname.split('/').filter(Boolean); + if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) { + return segments[0]; + } + } + + return DEFAULT_LOCALE; + }, [params, pathname]); } diff --git a/apps/website/src/lib/locales.ts b/apps/website/src/lib/locales.ts new file mode 100644 index 0000000..eb75dd0 --- /dev/null +++ b/apps/website/src/lib/locales.ts @@ -0,0 +1,25 @@ +export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const; +export type Locale = typeof SUPPORTED_LOCALES[number]; + +export const DEFAULT_LOCALE: Locale = 'th'; + +export function isSupportedLocale(locale: string): locale is Locale { + return SUPPORTED_LOCALES.includes(locale as Locale); +} + +export function getPathWithoutLocale(path: string): string { + const segments = path.split('/').filter(Boolean); + if (segments.length > 0 && isSupportedLocale(segments[0])) { + segments.shift(); + } + return '/' + segments.join('/'); +} + +export function getPathWithLocale(path: string, locale: string): string { + if (!path) return `/${locale}`; + if (path.startsWith('http')) return path; // Skip absolute external URLs + + const pathWithoutLocale = getPathWithoutLocale(path); + if (pathWithoutLocale === '/') return `/${locale}`; + return `/${locale}${pathWithoutLocale}`; +} diff --git a/apps/website/src/proxy.ts b/apps/website/src/proxy.ts new file mode 100644 index 0000000..9d73204 --- /dev/null +++ b/apps/website/src/proxy.ts @@ -0,0 +1,66 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from './lib/locales'; + +// Paths that should not be localized +const PUBLIC_FILE = /\.(.*)$/; +const SKIP_PATHS = ['/_next', '/api', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/sitemap']; + +export function proxy(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Skip Next.js internals, APIs, and public files + if ( + SKIP_PATHS.some((p) => pathname.startsWith(p)) || + PUBLIC_FILE.test(pathname) + ) { + return NextResponse.next(); + } + + // Check if the pathname already has a locale + const pathnameHasLocale = SUPPORTED_LOCALES.some( + (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` + ); + + if (pathnameHasLocale) { + return NextResponse.next(); + } + + // Determine the preferred locale + // 1. Check cookie + const cookieLocale = request.cookies.get('homaroma_lang')?.value; + let targetLocale = DEFAULT_LOCALE; + + if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) { + targetLocale = cookieLocale as any; + } else { + // 2. Check Accept-Language header + const acceptLang = request.headers.get('accept-language'); + if (acceptLang) { + const preferred = acceptLang.split(',')[0].split('-')[0].toLowerCase(); + if (SUPPORTED_LOCALES.includes(preferred as any)) { + targetLocale = preferred as any; + } + } + } + + // Redirect to the localized path + const newUrl = request.nextUrl.clone(); + newUrl.pathname = `/${targetLocale}${pathname === '/' ? '' : pathname}`; + + return NextResponse.redirect(newUrl); +} + +// Next.js 16 Proxy configuration +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico, sitemap.xml, robots.txt (metadata files) + */ + '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', + ], +}; diff --git a/fix_imports.js b/fix_imports.js new file mode 100644 index 0000000..e927ad8 --- /dev/null +++ b/fix_imports.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); + +function walkDir(dir, callback) { + fs.readdirSync(dir).forEach(f => { + let dirPath = path.join(dir, f); + let isDirectory = fs.statSync(dirPath).isDirectory(); + isDirectory ? walkDir(dirPath, callback) : callback(dirPath); + }); +} + +walkDir('apps/website/src/app/[lang]', (filePath) => { + if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) { + let content = fs.readFileSync(filePath, 'utf8'); + let changed = false; + + // Replace import ... from '../../lib/...' to import ... from '../../../lib/...' + // Wait, the files were moved 1 level deeper. So any import starting with '../' needs an extra '../' + // To be safe, let's just find and replace `from '../` with `from '../../` etc? + // Actually, converting to `@/` is safer: + // from '../../components/ => from '@/components/ + // from '../../../components/ => from '@/components/ + // from '../../lib/ => from '@/lib/ + // from '../../../lib/ => from '@/lib/ + // from '../components/ => from '@/components/ + // from '../lib/ => from '@/lib/ + + const newContent = content.replace(/from '(\.\.\/)+components\//g, "from '@/components/") + .replace(/from '(\.\.\/)+lib\//g, "from '@/lib/") + .replace(/from '(\.\.\/)+store\//g, "from '@/store/") + .replace(/from '(\.\.\/)+hooks\//g, "from '@/hooks/"); + + if (newContent !== content) { + fs.writeFileSync(filePath, newContent, 'utf8'); + console.log('Fixed', filePath); + } + } +}); diff --git a/fix_links.js b/fix_links.js new file mode 100644 index 0000000..0e11b5f --- /dev/null +++ b/fix_links.js @@ -0,0 +1,23 @@ +const fs = require('fs'); +const path = require('path'); + +function walkDir(dir, callback) { + fs.readdirSync(dir).forEach(f => { + let dirPath = path.join(dir, f); + let isDirectory = fs.statSync(dirPath).isDirectory(); + isDirectory ? walkDir(dirPath, callback) : callback(dirPath); + }); +} + +walkDir('apps/website/src/app/[lang]', (filePath) => { + if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) { + let content = fs.readFileSync(filePath, 'utf8'); + + const newContent = content.replace(/import Link from 'next\/link'/g, "import Link from '@/components/LocalizedLink'"); + + if (newContent !== content) { + fs.writeFileSync(filePath, newContent, 'utf8'); + console.log('Fixed link in', filePath); + } + } +}); diff --git a/fix_syntax.js b/fix_syntax.js new file mode 100644 index 0000000..2be78ad --- /dev/null +++ b/fix_syntax.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const path = require('path'); + +function walkDir(dir, callback) { + fs.readdirSync(dir).forEach(f => { + let dirPath = path.join(dir, f); + let isDirectory = fs.statSync(dirPath).isDirectory(); + isDirectory ? walkDir(dirPath, callback) : callback(dirPath); + }); +} + +walkDir('apps/website/src/app/[lang]', (filePath) => { + if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) { + let content = fs.readFileSync(filePath, 'utf8'); + + let newContent = content.replace(/\/\/ lang is extracted from paramsexport/g, "// lang is extracted from params\nexport") + .replace(/\/\/ lang is extracted from paramsfunction/g, "// lang is extracted from params\nfunction") + .replace(/\/\/ lang is extracted from paramstype/g, "// lang is extracted from params\ntype") + .replace(/\/\/ lang is extracted from paramsconst/g, "// lang is extracted from params\nconst"); + + if (filePath.endsWith('layout.tsx')) { + newContent = newContent.replace("import './globals.css';", "import '../globals.css';"); + } + + if (newContent !== content) { + fs.writeFileSync(filePath, newContent, 'utf8'); + console.log('Fixed syntax in', filePath); + } + } +});