feat(i18n): add multi-language support (en/zh) across app and server
Implement useLocale composable and shared translation dictionaries Translate public pages, booking flow, and receipt views Store booking locale to send localized WhatsApp notifications
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { AppLocale } from './i18n'
|
||||
|
||||
export type BookingMode = string
|
||||
export type TicketType = string
|
||||
export type BookingStatus = 'pending' | 'confirmed'
|
||||
@@ -41,6 +43,7 @@ export interface PublicBooking {
|
||||
event: DinnerEvent
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
locale: AppLocale
|
||||
bookingModeId: string | null
|
||||
bookingMode: BookingMode
|
||||
bookingModeLabel: string
|
||||
@@ -68,6 +71,7 @@ export interface ReceiptBooking {
|
||||
event: DinnerEvent
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
locale: AppLocale
|
||||
bookingModeId: string | null
|
||||
bookingMode: BookingMode
|
||||
bookingModeLabel: string
|
||||
@@ -158,11 +162,15 @@ export function isBookingStatus(value: string | null | undefined): value is Book
|
||||
return value === 'pending' || value === 'confirmed'
|
||||
}
|
||||
|
||||
export function getBookingStatusLabel(value: BookingStatus | string, label?: string | null) {
|
||||
if (label) {
|
||||
export function getBookingStatusLabel(value: BookingStatus | string, label?: string | null, locale: AppLocale = 'en') {
|
||||
if (label && locale !== 'zh') {
|
||||
return label
|
||||
}
|
||||
|
||||
if (locale === 'zh') {
|
||||
return value === 'confirmed' ? '已确认' : '等待负责人确认'
|
||||
}
|
||||
|
||||
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
|
||||
}
|
||||
|
||||
@@ -178,8 +186,8 @@ export function getBookingTicketLabel(booking: Pick<PublicBooking | ReceiptBooki
|
||||
return booking.ticketLabel || booking.ticketType.toUpperCase()
|
||||
}
|
||||
|
||||
export function formatBookingCurrency(value: number) {
|
||||
return new Intl.NumberFormat('en-MY', {
|
||||
export function formatBookingCurrency(value: number, locale: AppLocale = 'en') {
|
||||
return new Intl.NumberFormat(locale === 'zh' ? 'zh-MY' : 'en-MY', {
|
||||
style: 'currency',
|
||||
currency: 'MYR',
|
||||
minimumFractionDigits: 0,
|
||||
@@ -187,8 +195,8 @@ export function formatBookingCurrency(value: number) {
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function getSeatLabel(seatNumber: number) {
|
||||
return `Seat ${seatNumber}`
|
||||
export function getSeatLabel(seatNumber: number, locale: AppLocale = 'en') {
|
||||
return locale === 'zh' ? `座位 ${seatNumber}` : `Seat ${seatNumber}`
|
||||
}
|
||||
|
||||
export function calculateBookingInventorySummary(
|
||||
|
||||
43
shared/i18n.ts
Normal file
43
shared/i18n.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const SUPPORTED_LOCALES = ['en', 'zh'] as const
|
||||
export type AppLocale = typeof SUPPORTED_LOCALES[number]
|
||||
|
||||
export const DEFAULT_LOCALE: AppLocale = 'en'
|
||||
|
||||
export const LOCALE_OPTIONS: Array<{
|
||||
value: AppLocale
|
||||
label: string
|
||||
shortLabel: string
|
||||
}> = [
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
shortLabel: 'EN'
|
||||
},
|
||||
{
|
||||
value: 'zh',
|
||||
label: '中文',
|
||||
shortLabel: '中'
|
||||
}
|
||||
]
|
||||
|
||||
export function resolveLocale(value: string | null | undefined, fallback: AppLocale = DEFAULT_LOCALE): AppLocale {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (normalized === 'zh' || normalized.startsWith('zh-') || normalized.startsWith('cn')) {
|
||||
return 'zh'
|
||||
}
|
||||
|
||||
if (normalized === 'en' || normalized.startsWith('en-')) {
|
||||
return 'en'
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function getOppositeLocale(locale: AppLocale): AppLocale {
|
||||
return locale === 'zh' ? 'en' : 'zh'
|
||||
}
|
||||
Reference in New Issue
Block a user