diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index b6cc250..b6e3b61 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -49,7 +49,9 @@ export const Footer: React.FC = () => {

{locale === 'zh' ? '湘菜 · 辣度可调' - : 'อาหารหูหนาน (湘菜) · ปรับระดับความเผ็ดได้'} + : locale === 'en' + ? 'Hunan cuisine · Adjustable spice' + : 'อาหารหูหนาน (湘菜) · ปรับระดับความเผ็ดได้'}

diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a186bf0..75c2790 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -56,7 +56,7 @@ export const Header: React.FC = () => { role="group" aria-label="Language" > - + @@ -96,4 +108,3 @@ export const Header: React.FC = () => { ) } - diff --git a/src/i18n/I18nProvider.tsx b/src/i18n/I18nProvider.tsx index 75ed4de..cfc3d6c 100644 --- a/src/i18n/I18nProvider.tsx +++ b/src/i18n/I18nProvider.tsx @@ -7,7 +7,7 @@ const STORAGE_KEY = 'locale' export const I18nProvider: React.FC = ({ children }) => { const [locale, setLocaleState] = useState(() => { const fromStorage = window.localStorage.getItem(STORAGE_KEY) - if (fromStorage === 'th' || fromStorage === 'zh') return fromStorage + if (fromStorage === 'th' || fromStorage === 'zh' || fromStorage === 'en') return fromStorage return DEFAULT_LOCALE }) @@ -28,7 +28,9 @@ export const I18nProvider: React.FC = ({ children }) => document.title = locale === 'zh' ? '湘味小馆 | 正宗湘菜在泰国' - : 'Xiang Hunan Kitchen | อาหารหูหนานแท้ในไทย' + : locale === 'en' + ? 'Xiang Hunan Kitchen | Authentic Hunan Cuisine' + : 'Xiang Hunan Kitchen | อาหารหูหนานแท้ในไทย' }, [locale]) const value = useMemo( @@ -42,4 +44,3 @@ export const I18nProvider: React.FC = ({ children }) => return {children} } - diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 5a62fb0..aad0137 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -1,4 +1,4 @@ -export type Locale = 'th' | 'zh' +export type Locale = 'th' | 'zh' | 'en' export const DEFAULT_LOCALE: Locale = 'th' @@ -13,6 +13,7 @@ export const TRANSLATIONS: Record = { 'nav.news': 'ข่าวสาร', 'lang.th': 'ไทย', 'lang.zh': '中文', + 'lang.en': 'EN', 'home.hero.title': 'อาหารหูหนาน (湘菜) รสจัด เผ็ดหอม กลมกล่อม', 'home.hero.desc': @@ -87,9 +88,19 @@ export const TRANSLATIONS: Record = { 'common.call': 'โทร', 'common.contact': 'ติดต่อ', 'common.loadFailed': 'โหลดข้อมูลไม่สำเร็จ', + 'common.loading': 'กำลังโหลด', 'footer.contact': 'ติดต่อ', 'footer.hours': 'เวลาเปิด-ปิด', 'home.hoursPreview': '11:00–22:00', + + 'post.notFound': 'ไม่พบข้อมูลข่าวสาร', + 'categories.title': 'หมวดหมู่', + 'categories.subtitle': 'หน้านี้เป็นหน้ารวมหมวดหมู่จาก CMS (ยังไม่ได้ผูกเข้ากับเมนูอาหาร)', + 'categories.view': 'ดูรายการข่าวในหมวดนี้', + 'categories.empty': 'ยังไม่มีหมวดหมู่', + 'categoryDetail.titleFallback': 'หมวดหมู่', + 'categoryDetail.subtitle': 'ข่าวสารทั้งหมดในหมวดนี้', + 'categoryDetail.empty': 'ยังไม่มีข่าวสารในหมวดนี้', }, zh: { 'nav.home': '首页', @@ -99,6 +110,7 @@ export const TRANSLATIONS: Record = { 'nav.news': '新闻/活动', 'lang.th': 'ไทย', 'lang.zh': '中文', + 'lang.en': 'EN', 'home.hero.title': '正宗湘菜(湘菜) 香辣鲜香,层次分明', 'home.hero.desc': @@ -167,8 +179,116 @@ export const TRANSLATIONS: Record = { 'common.call': '拨打电话', 'common.contact': '联系', 'common.loadFailed': '加载失败', + 'common.loading': '加载中', 'footer.contact': '联系', 'footer.hours': '营业时间', 'home.hoursPreview': '11:00–22:00', + + 'post.notFound': '未找到该新闻内容', + 'categories.title': '分类', + 'categories.subtitle': '此页面展示 CMS 中的分类(尚未与菜品菜单关联)', + 'categories.view': '查看该分类下的新闻', + 'categories.empty': '暂无分类', + 'categoryDetail.titleFallback': '分类', + 'categoryDetail.subtitle': '该分类下的全部新闻', + 'categoryDetail.empty': '该分类下暂无新闻', + }, + en: { + 'nav.home': 'Home', + 'nav.menu': 'Menu', + 'nav.about': 'About', + 'nav.contact': 'Contact', + 'nav.news': 'News', + 'lang.th': 'TH', + 'lang.zh': '中文', + 'lang.en': 'EN', + + 'home.hero.title': 'Authentic Hunan Cuisine, Bold and Fragrant', + 'home.hero.desc': + 'A Hunan (Xiang) restaurant in Thailand with aromatic dry chilies and customizable heat - perfect for family meals and catch-ups with friends.', + 'home.hero.cta.menu': 'View menu', + 'home.hero.cta.contact': 'Reserve / Contact', + 'home.section.latestNews': 'Latest news', + 'home.section.latestNews.desc': 'Promotions, new dishes, and store updates', + 'home.section.latestNews.more': 'See all', + 'home.emptyNews': 'No news yet', + 'home.cta.title': 'Ready for a real Xiang kick?', + 'home.cta.desc': + 'Call or message us on Line to reserve a table - we will help you pick the right spice level.', + 'home.cta.call': 'Call to reserve', + 'home.cta.map': 'See map & hours', + + 'home.feature.1.title': 'Signature dry-chili aroma', + 'home.feature.1.desc': 'Stir-fried over high heat with bold spice and layered fragrance.', + 'home.feature.2.title': 'Made for sharing', + 'home.feature.2.desc': 'Perfect for sharing plates with friends and family.', + 'home.feature.3.title': 'Easy reservations', + 'home.feature.3.desc': 'Call or Line us for a table or dish recommendations.', + + 'menu.title': 'Menu', + 'menu.subtitle': + 'Bold Hunan flavors with aromatic dry chilies. Spice level can be adjusted to your taste.', + 'menu.allergy.title': 'Allergies or dietary needs?', + 'menu.allergy.desc': + 'Tell our staff before ordering. We can adjust recipes and recommend suitable dishes.', + 'menu.allergy.call': 'Call us', + 'menu.allergy.directions': 'Directions', + + 'about.title': 'Our story', + 'about.subtitle': + 'We want more people in Thailand to experience the charm of Hunan cuisine: fragrant dry chilies, balanced heat, and friendly service.', + 'about.occasions': 'Perfect for', + 'about.cta.title': 'Not sure what to order?', + 'about.cta.desc': + 'Call or Line us. Tell us your group size and spice preference - we will suggest a set.', + 'about.cta.call': 'Call to reserve', + 'about.cta.contact': 'Contact & directions', + + 'contact.title': 'Contact & directions', + 'contact.subtitle': 'Reservations, menu questions, or spice recommendations - reach us anytime.', + 'contact.shopInfo': 'Store info', + 'contact.phone': 'Phone', + 'contact.line': 'Line', + 'contact.address': 'Address', + 'contact.hours': 'Hours', + 'contact.mapTitle': 'Map', + 'contact.takeaway.title': 'Want takeaway?', + 'contact.takeaway.desc': + 'Call ahead for faster pickup, or message us on Line with your pickup time.', + 'contact.takeaway.call': 'Call to order', + 'contact.takeaway.line': 'Message on Line', + + 'news.title': 'News & promotions', + 'news.subtitle': 'Updates on promotions, events, and new dishes', + 'news.empty': 'No news yet', + + 'post.back': 'Back to news', + 'post.notFound': 'Sorry, this post is not available.', + + 'notFound.title': 'Page not found', + 'notFound.desc': 'The link may be incorrect, or the page has been moved.', + 'notFound.back': 'Back to home', + + 'card.more': 'Read more', + 'card.open': 'Open', + + 'common.phone': 'Phone', + 'common.hours': 'Hours', + 'common.call': 'Call', + 'common.contact': 'Contact', + 'common.loadFailed': 'Failed to load.', + 'common.loading': 'Loading', + 'footer.contact': 'Contact', + 'footer.hours': 'Hours', + 'home.hoursPreview': '11:00-22:00', + + 'categories.title': 'Categories', + 'categories.subtitle': + 'This page lists categories from the CMS (not connected to the food menu yet).', + 'categories.view': 'View posts in this category', + 'categories.empty': 'No categories yet', + 'categoryDetail.titleFallback': 'Category', + 'categoryDetail.subtitle': 'All posts in this category', + 'categoryDetail.empty': 'No posts in this category yet', }, } diff --git a/src/pages/Categories.tsx b/src/pages/Categories.tsx index 57ffd3c..023414e 100644 --- a/src/pages/Categories.tsx +++ b/src/pages/Categories.tsx @@ -7,6 +7,7 @@ import { createClient } from '../clientsdk/client' import { customQuerySerializer } from '../clientsdk/querySerializer' import type { Category } from '../clientsdk/types.gen' import { TENANT_API_KEY, TENANT_SLUG, API_URL } from '../config' +import { useI18n } from '../i18n/useI18n' type ListResponse = { docs: T[] } @@ -22,6 +23,7 @@ const client = createClient({ }) export const CategoriesPage: React.FC = () => { + const { t } = useI18n() const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -41,7 +43,7 @@ export const CategoriesPage: React.FC = () => { setCategories(response.data?.docs ?? []) } catch (err) { - setError(err instanceof Error ? err.message : 'โหลดข้อมูลไม่สำเร็จ') + setError(err instanceof Error ? err.message : t('common.loadFailed')) console.error('Fetch categories failed:', err) } finally { setLoading(false) @@ -49,7 +51,7 @@ export const CategoriesPage: React.FC = () => { } fetchCategories() - }, []) + }, [t]) return (
@@ -57,9 +59,9 @@ export const CategoriesPage: React.FC = () => {
-

หมวดหมู่

+

{t('categories.title')}

- หน้านี้เป็นหน้ารวมหมวดหมู่จาก CMS (ยังไม่ได้ผูกเข้ากับเมนูอาหาร) + {t('categories.subtitle')}

@@ -90,13 +92,13 @@ export const CategoriesPage: React.FC = () => { className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm transition hover:shadow-md" >

{category.title}

-

ดูรายการข่าวในหมวดนี้

+

{t('categories.view')}

))}
{!loading && categories.length === 0 && !error ? ( -
ยังไม่มีหมวดหมู่
+
{t('categories.empty')}
) : null} diff --git a/src/pages/CategoryDetail.tsx b/src/pages/CategoryDetail.tsx index 4d5a286..bc4ff55 100644 --- a/src/pages/CategoryDetail.tsx +++ b/src/pages/CategoryDetail.tsx @@ -10,6 +10,7 @@ import { customQuerySerializer } from '../clientsdk/querySerializer' import type { Category, Post } from '../clientsdk/types.gen' import { TENANT_API_KEY, TENANT_SLUG, API_URL } from '../config' import { isCategory } from '../utils/payload' +import { useI18n } from '../i18n/useI18n' type ListResponse = { docs: T[] } @@ -34,7 +35,7 @@ const stripHtml = (html: string): string => { const formatDate = (dateString: string): string => { const date = new Date(dateString) - return date.toLocaleDateString('th-TH', { + return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric', @@ -43,6 +44,7 @@ const formatDate = (dateString: string): string => { export const CategoryDetail: React.FC = () => { const { slug } = useParams<{ slug: string }>() + const { t } = useI18n() const [posts, setPosts] = useState([]) const [category, setCategory] = useState(null) @@ -87,7 +89,7 @@ export const CategoryDetail: React.FC = () => { setPosts(categoryPosts) } catch (err) { - setError(err instanceof Error ? err.message : 'โหลดข้อมูลไม่สำเร็จ') + setError(err instanceof Error ? err.message : t('common.loadFailed')) console.error('Fetch data failed:', err) } finally { setLoading(false) @@ -95,7 +97,7 @@ export const CategoryDetail: React.FC = () => { } fetchData() - }, [slug]) + }, [slug, t]) const items = useMemo(() => posts, [posts]) @@ -106,10 +108,10 @@ export const CategoryDetail: React.FC = () => {

- {category?.title ?? 'หมวดหมู่'} + {category?.title ?? t('categoryDetail.titleFallback')}

- ข่าวสารทั้งหมดในหมวดนี้ + {t('categoryDetail.subtitle')}

@@ -139,7 +141,7 @@ export const CategoryDetail: React.FC = () => { {!loading && items.length === 0 && !error ? (
- ยังไม่มีข่าวสารในหมวดนี้ + {t('categoryDetail.empty')}
) : null}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 8e7e1c3..3e062d0 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -34,7 +34,7 @@ const stripHtml = (html: string): string => { const formatDate = (dateString: string): string => { const date = new Date(dateString) - return date.toLocaleDateString('th-TH', { + return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric', diff --git a/src/pages/Menu.tsx b/src/pages/Menu.tsx index 03915cd..cbd4518 100644 --- a/src/pages/Menu.tsx +++ b/src/pages/Menu.tsx @@ -23,11 +23,13 @@ const formatPrice = (priceThb?: number) => { }).format(priceThb) } -const getSpicyLabel = (locale: 'th' | 'zh', level: MenuItem['spicyLevel']) => { - return locale === 'zh' ? `辣度 ${level}/3` : `ระดับความเผ็ด ${level}/3` +const getSpicyLabel = (locale: 'th' | 'zh' | 'en', level: MenuItem['spicyLevel']) => { + if (locale === 'zh') return `辣度 ${level}/3` + if (locale === 'en') return `Spice level ${level}/3` + return `ระดับความเผ็ด ${level}/3` } -const SpicyDots: React.FC<{ level: MenuItem['spicyLevel']; locale: 'th' | 'zh' }> = ({ +const SpicyDots: React.FC<{ level: MenuItem['spicyLevel']; locale: 'th' | 'zh' | 'en' }> = ({ level, locale, }) => { diff --git a/src/pages/News.tsx b/src/pages/News.tsx index aeff1d5..f3a78db 100644 --- a/src/pages/News.tsx +++ b/src/pages/News.tsx @@ -32,7 +32,7 @@ const stripHtml = (html: string): string => { const formatDate = (dateString: string): string => { const date = new Date(dateString) - return date.toLocaleDateString('th-TH', { + return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric', diff --git a/src/pages/PostDetail.tsx b/src/pages/PostDetail.tsx index 51cac49..d7c42d4 100644 --- a/src/pages/PostDetail.tsx +++ b/src/pages/PostDetail.tsx @@ -25,7 +25,7 @@ const client = createClient({ const formatDate = (dateString: string): string => { const date = new Date(dateString) - return date.toLocaleDateString('th-TH', { + return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric', @@ -112,7 +112,7 @@ export const PostDetail: React.FC = () => {
-

{error || 'ไม่พบข้อมูลข่าวสาร'}

+

{error || t('post.notFound')}