feat: Implement internationalization with English and Thai locales and a language switcher.

This commit is contained in:
supalerk-ar66 2026-01-19 15:51:28 +07:00
parent d6769ca1a9
commit ada40b05e8
8 changed files with 1951 additions and 3 deletions

View file

@ -26,18 +26,21 @@ const emit = defineEmits<{
</div>
<!-- Right Actions -->
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<!-- Search Bar (Optional) -->
<div v-if="showSearch !== false" class="relative hidden-mobile" style="width: 300px;">
<input
type="text"
class="input-field"
placeholder="ค้นหาคอร์ส..."
:placeholder="$t('menu.searchCourses')"
style="padding-left: 36px;"
>
<span style="position: absolute; left: 12px; top: 10px; color: var(--text-secondary);">🔍</span>
</div>
<!-- Language Switcher (Left of Avatar) -->
<LanguageSwitcher />
<!-- User Profile Dropdown -->
<UserMenu />
</div>

View file

@ -0,0 +1,140 @@
<script setup lang="ts">
/**
* @file LanguageSwitcher.vue
* @description Language switcher component using Quasar dropdown.
* Allows switching between Thai (th) and English (en) locales.
*/
const { locale, setLocale, locales } = useI18n()
// Get available locales with their names
const availableLocales = computed(() => {
return (locales.value as Array<{ code: string; name: string }>).map((loc) => ({
code: loc.code,
name: loc.name
}))
})
// Get current locale display (TH or EN)
const currentLocaleDisplay = computed(() => {
return locale.value.toUpperCase()
})
// Handle locale change
const changeLocale = async (code: string) => {
await setLocale(code as 'th' | 'en')
// Cookie is automatically handled by @nuxtjs/i18n with detectBrowserLanguage.useCookie
}
</script>
<template>
<q-btn
round
flat
class="language-btn"
:aria-label="$t('language.label')"
>
<span class="language-text">{{ currentLocaleDisplay }}</span>
<q-menu
anchor="bottom right"
self="top right"
class="language-menu"
>
<q-list style="min-width: 150px">
<q-item-label header class="text-xs font-bold uppercase tracking-wider" style="color: var(--text-secondary);">
{{ $t('language.label') }}
</q-item-label>
<q-item
v-for="loc in availableLocales"
:key="loc.code"
clickable
v-close-popup
@click="changeLocale(loc.code)"
:class="{ 'active-locale': locale === loc.code }"
class="language-item"
>
<q-item-section avatar>
<span class="text-lg">{{ loc.code === 'th' ? '🇹🇭' : '🇺🇸' }}</span>
</q-item-section>
<q-item-section>
<q-item-label class="font-semibold" style="color: var(--text-primary);">
{{ loc.name }}
</q-item-label>
<q-item-label caption style="color: var(--text-secondary);">
{{ loc.code.toUpperCase() }}
</q-item-label>
</q-item-section>
<q-item-section side v-if="locale === loc.code">
<q-icon name="check" color="primary" />
</q-item-section>
</q-item>
</q-list>
</q-menu>
<q-tooltip>{{ $t('language.label') }}</q-tooltip>
</q-btn>
</template>
<style scoped>
.language-btn {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary) 0%, #2f5ed7 100%);
color: white;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(75, 130, 247, 0.3);
}
.language-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(75, 130, 247, 0.4);
}
.language-text {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.5px;
}
.language-item {
border-radius: 8px;
margin: 4px 8px;
transition: all 0.2s ease;
}
.language-item:hover {
background: var(--bg-secondary);
}
.active-locale {
background: rgba(75, 130, 247, 0.1);
}
:deep(.language-menu) {
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
background: var(--bg-surface);
border: 1px solid var(--border-color);
}
:global(.dark) .language-btn {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}
:global(.dark) .language-btn:hover {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.5);
}
:global(.dark) :deep(.language-menu) {
background: #1e293b;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
:global(.dark) .active-locale {
background: rgba(59, 130, 246, 0.2);
}
</style>