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

View file

@ -0,0 +1,18 @@
/**
* @file i18n.config.ts
* @description Vue I18n configuration for the E-Learning Platform.
* Supports Thai (th) and English (en) locales.
*/
import th from './i18n/locales/th.json'
import en from './i18n/locales/en.json'
export default defineI18nConfig(() => ({
legacy: false,
locale: 'th',
fallbackLocale: 'th',
messages: {
th,
en
}
}))

View file

@ -0,0 +1,30 @@
{
"app": {
"title": "e-Learning System"
},
"dashboard": {
"welcomeTitle": "Welcome back",
"welcomeSubtitle": "Today is a great day to learn something new. Let's gain more knowledge!"
},
"menu": {
"continueLearning": "Continue Learning",
"recommendedCourses": "Recommended Courses",
"goToLesson": "Go to Full Lesson",
"viewDetails": "View Details",
"searchCourses": "Search courses..."
},
"course": {
"currentlyLearning": "Currently Learning",
"progress": "Progress",
"duration": "Duration"
},
"language": {
"label": "Language / ภาษา",
"thai": "ไทย",
"english": "English"
},
"common": {
"newBadge": "New",
"popularBadge": "Popular"
}
}

View file

@ -0,0 +1,30 @@
{
"app": {
"title": "ระบบ e-Learning"
},
"dashboard": {
"welcomeTitle": "ยินดีต้อนรับกลับ",
"welcomeSubtitle": "วันนี้เป็นวันที่ดีสำหรับการเรียนรู้สิ่งใหม่ๆ มาเก็บความรู้เพิ่มกันเถอะ"
},
"menu": {
"continueLearning": "เรียนต่อจากเดิม",
"recommendedCourses": "คอร์สเรียนแนะนำ",
"goToLesson": "เข้าสู่บทเรียนเต็มตัว",
"viewDetails": "ดูรายละเอียด",
"searchCourses": "ค้นหาคอร์ส..."
},
"course": {
"currentlyLearning": "กำลังเรียนอยู่",
"progress": "ความคืบหน้า",
"duration": "ระยะเวลา"
},
"language": {
"label": "ภาษา / Language",
"thai": "ไทย",
"english": "English"
},
"common": {
"newBadge": "ใหม่",
"popularBadge": "ยอดนิยม"
}
}

View file

@ -1,7 +1,23 @@
// Nuxt 3 + Quasar + Tailwind + TypeScript // Nuxt 3 + Quasar + Tailwind + TypeScript
// Configuration for E-Learning Platform // Configuration for E-Learning Platform
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ["nuxt-quasar-ui", "@nuxtjs/tailwindcss"], modules: ["nuxt-quasar-ui", "@nuxtjs/tailwindcss", "@nuxtjs/i18n"],
// i18n Configuration
i18n: {
strategy: 'no_prefix',
defaultLocale: 'th',
locales: [
{ code: 'th', name: 'ไทย', iso: 'th-TH' },
{ code: 'en', name: 'English', iso: 'en-US' }
],
vueI18n: './i18n.config.ts',
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root'
}
},
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css"],
typescript: { typescript: {
strict: true, strict: true,

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "^1.12.1", "@nuxt/eslint-config": "^1.12.1",
"@nuxtjs/i18n": "^10.2.1",
"@types/node": "^22.9.1", "@types/node": "^22.9.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"typescript": "^5.4.5" "typescript": "^5.4.5"