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:
2026-05-08 15:31:44 +08:00
parent b05cfd2c0e
commit 1318e766d5
14 changed files with 789 additions and 209 deletions

View File

@@ -4,39 +4,39 @@
<UCard class="border border-default bg-default shadow-sm">
<template #header>
<div class="space-y-2">
<UBadge label="Staff Access" color="primary" variant="soft" class="rounded-full" />
<UBadge :label="t('login.badge')" color="primary" variant="soft" class="rounded-full" />
<h1 class="text-3xl font-bold text-highlighted">
Login to the management system
{{ t('login.title') }}
</h1>
</div>
</template>
<UForm :state="form" :validate="validateLogin" class="space-y-5" @submit="onSubmit">
<UFormField name="username" label="Username" required>
<UFormField name="username" :label="t('login.username')" required>
<UInput
v-model="form.username"
type="text"
size="xl"
class="w-full"
placeholder="Enter your username"
:placeholder="t('login.usernamePlaceholder')"
/>
</UFormField>
<UFormField name="password" label="Password" required>
<UFormField name="password" :label="t('login.password')" required>
<UInput
v-model="form.password"
type="password"
size="xl"
class="w-full"
placeholder="Enter your password"
:placeholder="t('login.passwordPlaceholder')"
/>
</UFormField>
<UCheckbox v-model="form.remember" label="Remember this device" />
<UCheckbox v-model="form.remember" :label="t('login.remember')" />
<UButton
type="submit"
label="Sign In"
:label="t('login.signIn')"
size="xl"
:loading="passwordPending"
class="w-full justify-center"
@@ -45,13 +45,13 @@
<div class="my-6 flex items-center gap-3">
<div class="h-px flex-1 bg-default" />
<span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted">or</span>
<span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted">{{ t('login.or') }}</span>
<div class="h-px flex-1 bg-default" />
</div>
<div class="space-y-4">
<UButton
label="Sign In With Passkey"
:label="t('login.passkey')"
color="neutral"
variant="outline"
size="xl"
@@ -81,6 +81,7 @@ const toast = useToast()
const router = useRouter()
const auth = useAuth()
const apiClient = useApiClient()
const { t } = useLocale()
const form = reactive({
username: '',
@@ -94,11 +95,11 @@ function validateLogin(state: typeof form): FormError[] {
const errors: FormError[] = []
if (!state.username.trim()) {
errors.push({ name: 'username', message: 'Please enter your username.' })
errors.push({ name: 'username', message: t('login.usernameRequired') })
}
if (!state.password.trim()) {
errors.push({ name: 'password', message: 'Please enter your password.' })
errors.push({ name: 'password', message: t('login.passwordRequired') })
}
return errors
@@ -135,8 +136,8 @@ async function onSubmit(event: FormSubmitEvent<typeof form>) {
await finishLogin(response.user)
} catch (error: any) {
toast.add({
title: 'Login failed',
description: getErrorMessage(error, 'Unable to sign in with username and password.'),
title: t('login.failed'),
description: getErrorMessage(error, t('login.failedDescription')),
color: 'error',
icon: 'i-lucide-circle-alert'
})
@@ -181,8 +182,8 @@ async function loginWithPasskey() {
await finishLogin(verification.user)
} catch (error: any) {
toast.add({
title: 'Passkey login failed',
description: getErrorMessage(error, 'Unable to complete passkey login.'),
title: t('login.passkeyFailed'),
description: getErrorMessage(error, t('login.passkeyFailedDescription')),
color: 'error',
icon: 'i-lucide-circle-alert'
})