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:
@@ -40,7 +40,7 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-xs font-semibold uppercase tracking-[0.22em] text-muted">
|
||||
Dinner Ticket System
|
||||
{{ t('layout.brand') }}
|
||||
</div>
|
||||
<div class="truncate text-sm font-semibold text-highlighted">
|
||||
{{ auth.user.value.fullName }}
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
<UButton
|
||||
to="/"
|
||||
label="Public"
|
||||
:label="t('common.public')"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-house"
|
||||
@@ -113,9 +113,19 @@
|
||||
@click="mobileMenuOpen = false"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
:label="currentLocaleOption.label"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-languages"
|
||||
class="w-full justify-start rounded-2xl"
|
||||
:aria-label="t('common.switchLanguage')"
|
||||
@click="toggleLocale"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
:loading="logoutPending"
|
||||
label="Logout"
|
||||
:label="t('common.logout')"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-log-out"
|
||||
@@ -136,7 +146,7 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-xs font-semibold uppercase tracking-[0.22em] text-muted">
|
||||
Dinner Ticket System
|
||||
{{ t('layout.brand') }}
|
||||
</div>
|
||||
<div class="truncate text-sm font-semibold text-highlighted">
|
||||
{{ auth.user.value.fullName }}
|
||||
@@ -196,16 +206,26 @@
|
||||
<div class="grid gap-2">
|
||||
<UButton
|
||||
to="/"
|
||||
label="Public"
|
||||
:label="t('common.public')"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-house"
|
||||
class="justify-start rounded-2xl"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
:label="currentLocaleOption.label"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-languages"
|
||||
class="justify-start rounded-2xl"
|
||||
:aria-label="t('common.switchLanguage')"
|
||||
@click="toggleLocale"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
:loading="logoutPending"
|
||||
label="Logout"
|
||||
:label="t('common.logout')"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-log-out"
|
||||
@@ -227,7 +247,7 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-xs font-semibold uppercase tracking-[0.22em] text-muted">
|
||||
Dinner Ticket System
|
||||
{{ t('layout.brand') }}
|
||||
</div>
|
||||
<div class="truncate text-sm font-semibold text-highlighted">
|
||||
{{ auth.user.value.fullName }}
|
||||
@@ -239,7 +259,7 @@
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="shrink-0 rounded-full"
|
||||
:label="mobileMenuOpen ? 'Close' : 'Menu'"
|
||||
:label="mobileMenuOpen ? t('common.close') : t('common.menu')"
|
||||
:icon="mobileMenuOpen ? 'i-lucide-x' : 'i-lucide-menu'"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
aria-label="Toggle navigation"
|
||||
@@ -255,7 +275,7 @@
|
||||
|
||||
<footer class="border-t border-default bg-default/96">
|
||||
<UContainer class="py-5 text-center text-sm text-muted">
|
||||
© 2026 DAP 60th Anniversary Committee. All rights reserved.
|
||||
{{ t('layout.footer') }}
|
||||
</UContainer>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -273,20 +293,32 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-xs font-semibold uppercase tracking-[0.24em] text-muted">
|
||||
Dinner Ticket System
|
||||
{{ t('layout.brand') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
id="loginBtn"
|
||||
:to="route.path.startsWith('/login') ? '/' : '/login'"
|
||||
:label="route.path.startsWith('/login') ? 'Back' : 'Login'"
|
||||
color="neutral"
|
||||
:variant="route.path.startsWith('/login') ? 'outline' : 'solid'"
|
||||
:icon="route.path.startsWith('/login') ? 'i-lucide-arrow-left' : 'i-lucide-lock-keyhole'"
|
||||
class="rounded-full"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-languages"
|
||||
:label="currentLocaleOption.shortLabel"
|
||||
class="rounded-full"
|
||||
:aria-label="t('common.switchLanguage')"
|
||||
@click="toggleLocale"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
id="loginBtn"
|
||||
:to="route.path.startsWith('/login') ? '/' : '/login'"
|
||||
:label="route.path.startsWith('/login') ? t('common.back') : t('common.login')"
|
||||
color="neutral"
|
||||
:variant="route.path.startsWith('/login') ? 'outline' : 'solid'"
|
||||
:icon="route.path.startsWith('/login') ? 'i-lucide-arrow-left' : 'i-lucide-lock-keyhole'"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</header>
|
||||
@@ -297,7 +329,7 @@
|
||||
|
||||
<footer class="border-t border-default bg-default/96">
|
||||
<UContainer class="py-5 text-center text-sm text-muted">
|
||||
© 2026 DAP 60th Anniversary Committee. All rights reserved.
|
||||
{{ t('layout.footer') }}
|
||||
</UContainer>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -308,7 +340,7 @@
|
||||
import { getErrorMessage } from '../utils/errors'
|
||||
|
||||
interface SystemMenuItem {
|
||||
label: string
|
||||
labelKey: 'nav.bookings' | 'nav.security' | 'nav.users'
|
||||
to: string
|
||||
icon: string
|
||||
requiresSuperAdmin?: boolean
|
||||
@@ -320,26 +352,33 @@ const router = useRouter()
|
||||
const toast = useToast()
|
||||
const auth = useAuth()
|
||||
const apiClient = useApiClient()
|
||||
const { currentLocaleOption, locale, t, toggleLocale } = useLocale()
|
||||
const logoutPending = ref(false)
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
await auth.fetchSession()
|
||||
|
||||
useHead(() => ({
|
||||
htmlAttrs: {
|
||||
lang: locale.value === 'zh' ? 'zh-Hans' : 'en'
|
||||
}
|
||||
}))
|
||||
|
||||
const allSystemMenuItems: SystemMenuItem[] = [
|
||||
{
|
||||
label: 'Bookings',
|
||||
labelKey: 'nav.bookings',
|
||||
to: '/bookings',
|
||||
icon: 'i-lucide-clipboard-list',
|
||||
matches: (path) => path.startsWith('/bookings')
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
labelKey: 'nav.security',
|
||||
to: '/security',
|
||||
icon: 'i-lucide-shield-check',
|
||||
matches: (path) => path.startsWith('/security')
|
||||
},
|
||||
{
|
||||
label: 'Users',
|
||||
labelKey: 'nav.users',
|
||||
to: '/management/users',
|
||||
icon: 'i-lucide-users',
|
||||
requiresSuperAdmin: true,
|
||||
@@ -348,13 +387,18 @@ const allSystemMenuItems: SystemMenuItem[] = [
|
||||
]
|
||||
|
||||
const systemMenuItems = computed(() => {
|
||||
return allSystemMenuItems.filter((item) => {
|
||||
return !item.requiresSuperAdmin || auth.isSuperAdmin.value
|
||||
})
|
||||
return allSystemMenuItems
|
||||
.filter((item) => {
|
||||
return !item.requiresSuperAdmin || auth.isSuperAdmin.value
|
||||
})
|
||||
.map((item) => ({
|
||||
...item,
|
||||
label: t(item.labelKey)
|
||||
}))
|
||||
})
|
||||
|
||||
const userRoleLabel = computed(() => {
|
||||
return auth.isSuperAdmin.value ? 'Super Admin' : 'Staff'
|
||||
return auth.isSuperAdmin.value ? t('role.superAdmin') : t('role.staff')
|
||||
})
|
||||
|
||||
function isMenuItemActive(item: SystemMenuItem) {
|
||||
@@ -405,15 +449,15 @@ async function logout() {
|
||||
await router.push('/login')
|
||||
|
||||
toast.add({
|
||||
title: 'Signed out',
|
||||
description: 'Your session has been cleared.',
|
||||
title: t('layout.signedOut'),
|
||||
description: t('layout.sessionCleared'),
|
||||
color: 'success',
|
||||
icon: 'i-lucide-check-circle-2'
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.add({
|
||||
title: 'Logout failed',
|
||||
description: getErrorMessage(error, 'Unable to end the current session.'),
|
||||
title: t('layout.logoutFailed'),
|
||||
description: getErrorMessage(error, t('layout.logoutFailedDescription')),
|
||||
color: 'error',
|
||||
icon: 'i-lucide-circle-alert'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user