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 = () => {
@@ -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 = () => {
@@ -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')}