import { DEFAULT_LOCALE, LOCALE_OPTIONS, getOppositeLocale, resolveLocale, type AppLocale } from '~~/shared/i18n' const messages = { en: { 'common.back': 'Back', 'common.cancel': 'Cancel', 'common.category': 'Category', 'common.date': 'Date', 'common.login': 'Login', 'common.logout': 'Logout', 'common.menu': 'Menu', 'common.close': 'Close', 'common.open': 'Open', 'common.phoneNumber': 'Phone Number', 'common.price': 'Price', 'common.public': 'Public', 'common.refresh': 'Refresh', 'common.seats': 'Seats', 'common.status': 'Status', 'common.time': 'Time', 'common.totalPrice': 'Total Price', 'common.venue': 'Venue', 'common.switchLanguage': 'Switch language', 'common.language': 'Language', 'layout.brand': 'Dinner Ticket System', 'layout.footer': '© 2026 DAP 60th Anniversary Committee. All rights reserved.', 'layout.signedOut': 'Signed out', 'layout.sessionCleared': 'Your session has been cleared.', 'layout.logoutFailed': 'Logout failed', 'layout.logoutFailedDescription': 'Unable to end the current session.', 'nav.bookings': 'Bookings', 'nav.security': 'Security', 'nav.users': 'Users', 'role.superAdmin': 'Super Admin', 'role.staff': 'Staff', 'booking.name': 'Name', 'booking.namePlaceholder': 'e.g. John Doe', 'booking.phonePlaceholder': 'e.g. +60123456789', 'booking.bookingMode': 'Booking Mode', 'booking.quantity': 'Quantity', 'booking.ticketCategory': 'Ticket Category', 'booking.personInCharge': 'Person In Charge', 'booking.bookNow': 'Book Now', 'booking.seatGeneration': 'This booking will generate {count} {seatLabel}.', 'booking.noPicTitle': 'No person in charge available', 'booking.noPicDescription': 'Add a user with a phone number in the management page first.', 'booking.whatsappOpened': 'WhatsApp booking draft opened', 'booking.whatsappOpenedDescription': 'Booking details and the confirmation link were sent to {name}.', 'booking.createFailed': 'Booking could not be created', 'booking.tryAgain': 'Please try again in a moment.', 'booking.nameRequired': 'Please enter the guest or organizer name.', 'booking.phoneRequired': 'Please enter a contact number.', 'booking.phoneInvalid': 'Use a valid phone number with country code, e.g. +60123456789.', 'booking.quantityMin': '{label} must be at least 1.', 'booking.modeRequired': 'Please select a booking mode.', 'booking.ticketRequired': 'Please select a ticket category.', 'confirm.badge': 'PIC Confirmation', 'confirm.title': 'Review Booking Details', 'confirm.description': 'Confirm the booking after verifying the details below.', 'confirm.status': 'Booking status', 'confirm.submitted': 'Submitted {date}', 'confirm.alreadyConfirmed': 'Booking already confirmed', 'confirm.confirmedOn': 'Confirmed on {date}.', 'confirm.guestOrganizer': 'Guest / Organizer', 'confirm.contactNumber': 'Contact Number', 'confirm.pic': 'Person In Charge', 'confirm.picPhone': 'PIC Phone', 'confirm.ticketCategory': 'Ticket Category', 'confirm.seatsCovered': 'Seats Covered', 'confirm.submittedLabel': 'Submitted', 'confirm.confirmedAt': 'Confirmed At', 'confirm.backToForm': 'Back To Booking Form', 'confirm.openReceipt': 'Open Ticket Receipt', 'confirm.confirmBooking': 'Confirm This Booking', 'confirm.cancelConfirmation': 'Cancel Confirmation', 'confirm.alreadyConfirmedToast': 'Booking already confirmed', 'confirm.confirmedToast': 'Booking confirmed', 'confirm.alreadyConfirmedDescription': 'This booking had already been confirmed earlier.', 'confirm.receiptSent': 'Ticket receipt was sent to {phone}.', 'confirm.receiptNotSent': 'Booking confirmed, but the ticket receipt WhatsApp was not sent: {error}', 'confirm.failed': 'Confirmation failed', 'confirm.cancelPrompt': 'Cancel this confirmation? The booking will return to pending and the seats will be released.', 'confirm.alreadyPending': 'Booking already pending', 'confirm.cancelled': 'Confirmation cancelled', 'confirm.alreadyPendingDescription': 'This booking was already pending confirmation.', 'confirm.cancelledDescription': 'The booking has been returned to pending status.', 'confirm.cancelFailed': 'Cancellation failed', 'receipt.badge': 'Ticket Receipt', 'receipt.mainQr': 'Main QR', 'receipt.seatList': 'Seat List', 'receipt.shareSeats': 'Share Seats', 'receipt.batchShare': 'Batch Share', 'receipt.shared': 'shared', 'receipt.available': 'available', 'receipt.seatDetail': 'Seat Detail', 'receipt.openLink': 'Open Link', 'receipt.share': 'Share', 'receipt.sharedStatus': 'Shared', 'receipt.availableStatus': 'Available', 'receipt.unassigned': 'Unassigned', 'receipt.readyToShare': 'Ready to share', 'receipt.sharedAt': 'Shared {date}', 'receipt.openSeatLink': 'Open seat link', 'receipt.shareSeat': 'Share seat', 'receipt.unshareSeat': 'Unshare seat', 'receipt.unshare': 'Unshare', 'receipt.batchTitle': 'Batch Share Seats', 'receipt.seatsToShare': 'Seats To Share', 'receipt.nextSeats': 'Next Seats', 'receipt.noSeatsAvailable': 'No seats available', 'receipt.recipientName': 'Recipient Name', 'receipt.recipientPhone': 'Recipient Phone', 'receipt.optional': 'Optional', 'receipt.optionalPhonePlaceholder': 'Optional, e.g. +60123456789', 'receipt.recipient': 'Recipient', 'receipt.recipientPhoneLabel': 'Recipient Phone', 'receipt.guest': 'Guest', 'receipt.seatsReady': 'Seats ready', 'receipt.seatsReadyDescription': '{count} {seatLabel} ready to send.', 'receipt.copied': 'Copied to clipboard.', 'receipt.copyPrompt': 'Copy this text', 'receipt.seatUpdateFailed': 'Seat update failed', 'receipt.seatUpdateFailedDescription': 'The share sheet opened, but the seat records could not be updated.', 'receipt.seatsShared': '{count} {seatLabel} shared', 'receipt.allSeatsSent': 'Next available seats were sent.', 'receipt.someSeatsFailed': 'Some seats were sent, but at least one update failed.', 'receipt.unableShareSeats': 'Unable to share seats', 'receipt.seatReady': 'Seat ready', 'receipt.seatReadyDescription': '{seat} is ready to send.', 'receipt.seatShared': '{seat} shared', 'receipt.unableShareSeat': 'Unable to share seat', 'receipt.unsharePrompt': 'Unshare {seat}? The previous link will stop working.', 'receipt.seatUnshared': '{seat} unshared', 'receipt.unableUnshareSeat': 'Unable to unshare seat', 'login.badge': 'Staff Access', 'login.title': 'Login to the management system', 'login.username': 'Username', 'login.usernamePlaceholder': 'Enter your username', 'login.password': 'Password', 'login.passwordPlaceholder': 'Enter your password', 'login.remember': 'Remember this device', 'login.signIn': 'Sign In', 'login.or': 'or', 'login.passkey': 'Sign In With Passkey', 'login.usernameRequired': 'Please enter your username.', 'login.passwordRequired': 'Please enter your password.', 'login.failed': 'Login failed', 'login.failedDescription': 'Unable to sign in with username and password.', 'login.passkeyFailed': 'Passkey login failed', 'login.passkeyFailedDescription': 'Unable to complete passkey login.', 'date.notAvailable': 'Not available' }, zh: { 'common.back': '返回', 'common.cancel': '取消', 'common.category': '类别', 'common.date': '日期', 'common.login': '登录', 'common.logout': '退出登录', 'common.menu': '菜单', 'common.close': '关闭', 'common.open': '打开', 'common.phoneNumber': '联络号码', 'common.price': '价格', 'common.public': '公开页面', 'common.refresh': '刷新', 'common.seats': '座位', 'common.status': '状态', 'common.time': '时间', 'common.totalPrice': '总价', 'common.venue': '地点', 'common.switchLanguage': '切换语言', 'common.language': '语言', 'layout.brand': '晚宴票券系统', 'layout.footer': '© 2026 DAP 60周年委员会。版权所有。', 'layout.signedOut': '已退出登录', 'layout.sessionCleared': '您的登录会话已清除。', 'layout.logoutFailed': '退出登录失败', 'layout.logoutFailedDescription': '无法结束当前会话。', 'nav.bookings': '预订', 'nav.security': '安全设置', 'nav.users': '用户', 'role.superAdmin': '超级管理员', 'role.staff': '职员', 'booking.name': '姓名', 'booking.namePlaceholder': '例如:陈大文', 'booking.phonePlaceholder': '例如:+60123456789', 'booking.bookingMode': '预订方式', 'booking.quantity': '数量', 'booking.ticketCategory': '票券类别', 'booking.personInCharge': '负责人', 'booking.bookNow': '立即预订', 'booking.seatGeneration': '此预订将生成 {count} 个{seatLabel}。', 'booking.noPicTitle': '没有可用负责人', 'booking.noPicDescription': '请先在管理页面新增一个有电话号码的用户。', 'booking.whatsappOpened': 'WhatsApp 预订草稿已打开', 'booking.whatsappOpenedDescription': '预订详情和确认链接已发送给 {name}。', 'booking.createFailed': '无法创建预订', 'booking.tryAgain': '请稍后再试。', 'booking.nameRequired': '请输入宾客或订桌人姓名。', 'booking.phoneRequired': '请输入联络号码。', 'booking.phoneInvalid': '请输入包含国家区号的有效号码,例如 +60123456789。', 'booking.quantityMin': '{label} 至少为 1。', 'booking.modeRequired': '请选择预订方式。', 'booking.ticketRequired': '请选择票券类别。', 'confirm.badge': '负责人确认', 'confirm.title': '检查预订详情', 'confirm.description': '请核对以下资料后确认预订。', 'confirm.status': '预订状态', 'confirm.submitted': '提交于 {date}', 'confirm.alreadyConfirmed': '预订已确认', 'confirm.confirmedOn': '确认于 {date}。', 'confirm.guestOrganizer': '宾客 / 订桌人', 'confirm.contactNumber': '联络号码', 'confirm.pic': '负责人', 'confirm.picPhone': '负责人电话', 'confirm.ticketCategory': '票券类别', 'confirm.seatsCovered': '座位数量', 'confirm.submittedLabel': '提交时间', 'confirm.confirmedAt': '确认时间', 'confirm.backToForm': '返回预订表格', 'confirm.openReceipt': '打开票券收据', 'confirm.confirmBooking': '确认此预订', 'confirm.cancelConfirmation': '取消确认', 'confirm.alreadyConfirmedToast': '预订已确认', 'confirm.confirmedToast': '预订已确认', 'confirm.alreadyConfirmedDescription': '此预订之前已经确认。', 'confirm.receiptSent': '票券收据已发送至 {phone}。', 'confirm.receiptNotSent': '预订已确认,但 WhatsApp 票券收据未发送:{error}', 'confirm.failed': '确认失败', 'confirm.cancelPrompt': '确定取消此确认?预订会回到待确认状态,座位也会释放。', 'confirm.alreadyPending': '预订已是待确认', 'confirm.cancelled': '已取消确认', 'confirm.alreadyPendingDescription': '此预订已经处于待确认状态。', 'confirm.cancelledDescription': '预订已回到待确认状态。', 'confirm.cancelFailed': '取消失败', 'receipt.badge': '票券收据', 'receipt.mainQr': '主二维码', 'receipt.seatList': '座位列表', 'receipt.shareSeats': '分享座位', 'receipt.batchShare': '批量分享', 'receipt.shared': '已分享', 'receipt.available': '可分享', 'receipt.seatDetail': '座位详情', 'receipt.openLink': '打开链接', 'receipt.share': '分享', 'receipt.sharedStatus': '已分享', 'receipt.availableStatus': '可分享', 'receipt.unassigned': '未指定', 'receipt.readyToShare': '可分享', 'receipt.sharedAt': '分享于 {date}', 'receipt.openSeatLink': '打开座位链接', 'receipt.shareSeat': '分享座位', 'receipt.unshareSeat': '取消分享座位', 'receipt.unshare': '取消分享', 'receipt.batchTitle': '批量分享座位', 'receipt.seatsToShare': '要分享的座位', 'receipt.nextSeats': '接下来的座位', 'receipt.noSeatsAvailable': '没有可分享的座位', 'receipt.recipientName': '接收人姓名', 'receipt.recipientPhone': '接收人电话', 'receipt.optional': '选填', 'receipt.optionalPhonePlaceholder': '选填,例如 +60123456789', 'receipt.recipient': '接收人', 'receipt.recipientPhoneLabel': '接收人电话', 'receipt.guest': '宾客', 'receipt.seatsReady': '座位已准备好', 'receipt.seatsReadyDescription': '{count} 个{seatLabel}已准备发送。', 'receipt.copied': '已复制到剪贴板。', 'receipt.copyPrompt': '复制这段文字', 'receipt.seatUpdateFailed': '座位更新失败', 'receipt.seatUpdateFailedDescription': '分享面板已打开,但座位记录无法更新。', 'receipt.seatsShared': '已分享 {count} 个{seatLabel}', 'receipt.allSeatsSent': '接下来的可用座位已发送。', 'receipt.someSeatsFailed': '部分座位已发送,但至少一个座位更新失败。', 'receipt.unableShareSeats': '无法分享座位', 'receipt.seatReady': '座位已准备好', 'receipt.seatReadyDescription': '{seat} 已准备发送。', 'receipt.seatShared': '{seat} 已分享', 'receipt.unableShareSeat': '无法分享座位', 'receipt.unsharePrompt': '确定取消分享 {seat}?旧链接将无法继续使用。', 'receipt.seatUnshared': '{seat} 已取消分享', 'receipt.unableUnshareSeat': '无法取消分享座位', 'login.badge': '职员入口', 'login.title': '登录管理系统', 'login.username': '用户名', 'login.usernamePlaceholder': '请输入用户名', 'login.password': '密码', 'login.passwordPlaceholder': '请输入密码', 'login.remember': '记住此设备', 'login.signIn': '登录', 'login.or': '或', 'login.passkey': '使用 Passkey 登录', 'login.usernameRequired': '请输入用户名。', 'login.passwordRequired': '请输入密码。', 'login.failed': '登录失败', 'login.failedDescription': '无法使用用户名和密码登录。', 'login.passkeyFailed': 'Passkey 登录失败', 'login.passkeyFailedDescription': '无法完成 Passkey 登录。', 'date.notAvailable': '无资料' } } as const type MessageKey = keyof typeof messages.en type MessageParams = Record function interpolate(message: string, params?: MessageParams) { if (!params) { return message } return message.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? '')) } export function useLocale() { const localeCookie = useCookie('dts-locale', { sameSite: 'lax' }) const requestHeaders = import.meta.server ? useRequestHeaders(['accept-language']) : {} const locale = useState('dts-locale', () => resolveLocale( localeCookie.value || requestHeaders['accept-language'], DEFAULT_LOCALE )) if (import.meta.client && !localeCookie.value) { locale.value = resolveLocale(navigator.language, locale.value) } localeCookie.value = locale.value function setLocale(nextLocale: string) { locale.value = resolveLocale(nextLocale, locale.value) localeCookie.value = locale.value } function toggleLocale() { setLocale(getOppositeLocale(locale.value)) } function t(key: MessageKey, params?: MessageParams) { const message = messages[locale.value][key] || messages.en[key] || key return interpolate(message, params) } const localeOptions = LOCALE_OPTIONS const currentLocaleOption = computed(() => { return localeOptions.find((option) => option.value === locale.value) || localeOptions[0] }) return { locale, localeOptions, currentLocaleOption, setLocale, toggleLocale, t } }