feat: Implement internationalization with English and Thai locales and a language switcher.
This commit is contained in:
parent
d6769ca1a9
commit
ada40b05e8
8 changed files with 1951 additions and 3 deletions
|
|
@ -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>
|
||||
|
|
|
|||
140
Frontend-Learner/components/common/LanguageSwitcher.vue
Normal file
140
Frontend-Learner/components/common/LanguageSwitcher.vue
Normal 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>
|
||||
18
Frontend-Learner/i18n.config.ts
Normal file
18
Frontend-Learner/i18n.config.ts
Normal 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
|
||||
}
|
||||
}))
|
||||
30
Frontend-Learner/i18n/locales/en.json
Normal file
30
Frontend-Learner/i18n/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
30
Frontend-Learner/i18n/locales/th.json
Normal file
30
Frontend-Learner/i18n/locales/th.json
Normal 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": "ยอดนิยม"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,23 @@
|
|||
// Nuxt 3 + Quasar + Tailwind + TypeScript
|
||||
// Configuration for E-Learning Platform
|
||||
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"],
|
||||
typescript: {
|
||||
strict: true,
|
||||
|
|
|
|||
1710
Frontend-Learner/package-lock.json
generated
1710
Frontend-Learner/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^1.12.1",
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@types/node": "^22.9.1",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.4.5"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue