feat: Implement internationalization (i18n) with language switching and establish core backend services and frontend pages for course management.

This commit is contained in:
supalerk-ar66 2026-01-19 15:11:47 +07:00
parent 6a05e6fdb6
commit dbf62feea9
13 changed files with 3195 additions and 1463 deletions

View file

@ -38,6 +38,9 @@ const emit = defineEmits<{
<span style="position: absolute; left: 12px; top: 10px; color: var(--text-secondary);">🔍</span>
</div>
<!-- Language Switcher -->
<LanguageSwitcher />
<!-- User Profile Dropdown -->
<UserMenu />
</div>

View file

@ -0,0 +1,89 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { locale, setLocale } = useI18n()
// Language options
const languages = [
{ code: 'th', label: 'TH', fullLabel: 'ไทย', flagSrc: '/flags/th.svg' },
{ code: 'en', label: 'EN', fullLabel: 'English', flagSrc: '/flags/en.svg' }
]
// Get current language object
const currentLang = computed(() =>
languages.find(l => l.code === locale.value) || languages[0]
)
// Change language function
const changeLanguage = (langCode: string) => {
setLocale(langCode)
}
</script>
<template>
<q-btn
rounded
flat
no-caps
class="text-slate-700 dark:text-slate-200"
:aria-label="$t('app.title') || 'Change Language'"
>
<div class="flex items-center gap-2">
<img
:src="currentLang.flagSrc"
alt="flag"
class="w-5 h-5 rounded-full object-cover border border-slate-200 dark:border-slate-600"
/>
<span class="font-bold">{{ currentLang.label }}</span>
<q-icon name="arrow_drop_down" />
</div>
<!-- Tooltip -->
<q-tooltip>ภาษา / Language</q-tooltip>
<!-- Dropdown Menu -->
<q-menu
auto-close
transition-show="jump-down"
transition-hide="jump-up"
class="rounded-xl shadow-lg border border-slate-100 bg-white dark:bg-slate-800 dark:border-slate-700"
>
<q-list style="min-width: 150px" class="py-2">
<q-item
v-for="lang in languages"
:key="lang.code"
clickable
v-close-popup
:active="locale === lang.code"
active-class="bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
class="text-black dark:text-white hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
@click="changeLanguage(lang.code)"
>
<q-item-section avatar>
<img
:src="lang.flagSrc"
alt="flag"
class="w-6 h-6 rounded-full object-cover border border-slate-200 dark:border-slate-600"
/>
</q-item-section>
<q-item-section>
<span :class="{ 'font-bold': locale === lang.code }">{{ lang.fullLabel }}</span>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
<style scoped>
/* Optional: adjust Quasar overrides if needed */
</style>
<style scoped>
.font-emoji {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
/* Tooltip directive placeholder if not globally available,
though usually handled by a library like Floating Vue or simple title attr */
</style>

View file

@ -0,0 +1,9 @@
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,171 @@
{
"app": {
"name": "E-Learning Platform",
"title": "E-Learning System",
"systemTitle": "E-Learning System - Online Learning Platform"
},
"common": {
"loading": "Loading...",
"views": "Views",
"students": "Students",
"lessons": "Lessons",
"hours": "Hours"
},
"dashboard": {
"welcomeTitle": "Welcome back, {name}!",
"welcomeSubtitle": "Today is a great day to learn something new. Let's gain more knowledge.",
"continueLearning": "Continue Learning",
"enterLearning": "Enter Full Lesson",
"recommendedCourses": "Recommended Courses",
"currentLearning": "CURRENTLY LEARNING",
"level": "Chapter",
"progress": "PROGRESS",
"viewDetails": "View Details",
"newBadge": "New",
"popularBadge": "Popular"
},
"auth": {
"login": {
"title": "Login",
"welcome": "Welcome back! Please enter your details.",
"submitButton": "Login",
"googleLogin": "Login with Google",
"rememberMe": "Remember me",
"forgotPassword": "Forgot password?",
"noAccount": "Don't have an account?",
"registerNow": "Register"
},
"register": {
"title": "Register",
"welcome": "Welcome back! Please enter your details.",
"submitButton": "Create Account",
"hasAccount": "Already have an account?",
"loginNow": "Login",
"successMessage": "Registration successful! Please login.",
"failMessage": "Registration failed"
},
"form": {
"username": "Username",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm Password",
"prefix": "Prefix",
"firstName": "First Name",
"lastName": "Last Name",
"phone": "Phone Number",
"prefixOptions": {
"mr": "Mr.",
"mrs": "Mrs.",
"ms": "Ms."
},
"placeholders": {
"email": "student{'@'}example.com",
"password": "••••••••",
"createPassword": "Create password (min 8 chars)",
"confirmPassword": "Confirm password again",
"username": "username"
}
},
"validation": {
"required": "This field is required",
"emailInvalid": "Please enter a valid email",
"passwordLength": "Password must be at least 8 characters",
"noThai": "Thai characters are not allowed",
"noNumber": "Numbers are not allowed",
"onlyNumber": "Please enter numbers only",
"passwordMismatch": "Passwords do not match",
"loginFailed": "Please check your Email or Password again."
},
"backToHome": "Back to Home"
},
"landing": {
"header": {
"menu": {
"allCourses": "All Courses",
"discovery": "Discovery"
},
"actions": {
"login": "Login",
"getStarted": "Get Started",
"enterDashboard": "Enter Dashboard"
}
},
"hero": {
"badge": "🚀 Start your new path to success here",
"titlePart1": "Level Up Your",
"titleHighlight": "Future Skills",
"titlePart2": "With Us",
"subtitle": "The most accessible online knowledge hub. Developed by experts to help you reach your goals with confidence.",
"ctaStart": "Start Learning for Free",
"ctaBrowse": "Browse All Courses"
},
"features": {
"certificate": {
"title": "Get Certified",
"desc": "Upon course completion"
},
"anywhere": {
"title": "Learn Anywhere",
"desc": "Supports all devices"
}
},
"whyChooseUs": {
"badge": "Why Choose Us",
"title": "Designed for Your Success",
"desc": "We are not just a learning platform, but a partner to help you reach your destination.",
"items": {
"material": {
"title": "High Quality Material",
"desc": "Sharp video quality with carefully selected learning materials for easy understanding."
},
"assessment": {
"title": "Smart Assessment",
"desc": "Online Quiz system to immediately assess understanding with improvement analysis."
},
"tracking": {
"title": "Progress Tracking",
"desc": "Track your progress anytime, anywhere via a Dashboard that summarizes everything perfectly."
}
}
},
"footer": {
"brandDesc": "Online learning platform for future skills",
"services": {
"title": "All Services",
"onlineCourses": "Online Courses",
"onsite": "Onsite Courses",
"classSystem": "Class System",
"forOrg": "For Organizations"
},
"about": {
"title": "About Us",
"yearlyPackage": "Yearly Package",
"learningPath": "Learning Path",
"skillCheck": "Skill Check",
"articles": "Articles",
"faq": "FAQ"
},
"join": {
"title": "Join Us",
"jobs": "Careers",
"affiliate": "Affiliate Program",
"instructor": "Become an Instructor",
"aboutUs": "About Us",
"contact": "Contact Us"
},
"download": {
"title": "Download Application"
},
"consult": {
"title": "Consultation",
"addLine": "Add Line Friend"
},
"legal": {
"terms": "Terms of Service",
"privacy": "Privacy Policy",
"refund": "Refund Policy",
"copyright": "© Copyright 2019-2026 LIKE ME X CO.,LTD All rights reserved."
}
}
}
}

View file

@ -0,0 +1,171 @@
{
"app": {
"name": "แพลตฟอร์มการเรียนรู้ออนไลน์",
"title": "ระบบ E-Learning",
"systemTitle": "ระบบการเรียนรู้ออนไลน์เพื่อพัฒนาทักษะแห่งอนาคต"
},
"common": {
"loading": "กำลังโหลด...",
"views": "ยอดเข้าชม",
"students": "ผู้เรียน",
"lessons": "บทเรียน",
"hours": "ชั่วโมง"
},
"dashboard": {
"welcomeTitle": "ยินดีต้อนรับกลับ, {name}!",
"welcomeSubtitle": "วันนี้เป็นวันที่ดีสำหรับการเรียนรู้สิ่งใหม่ๆ มาเพิ่มพูนความรู้กันเถอะ",
"continueLearning": "เรียนต่อจากเดิม",
"enterLearning": "เข้าสู่บทเรียนเต็มตัว",
"recommendedCourses": "คอร์สเรียนแนะนำ",
"currentLearning": "กำลังเรียน",
"level": "บทที่",
"progress": "ความคืบหน้า",
"viewDetails": "ดูรายละเอียด",
"newBadge": "ใหม่",
"popularBadge": "ยอดนิยม"
},
"auth": {
"login": {
"title": "เข้าสู่ระบบ",
"welcome": "ยินดีต้อนรับกลับ! กรุณากรอกข้อมูลเพื่อเข้าสู่ระบบ",
"submitButton": "เข้าสู่ระบบ",
"googleLogin": "เข้าสู่ระบบด้วย Google",
"rememberMe": "จำฉันไว้ในระบบ",
"forgotPassword": "ลืมรหัสผ่าน?",
"noAccount": "ยังไม่มีบัญชีผู้ใช้?",
"registerNow": "สมัครสมาชิก"
},
"register": {
"title": "สมัครสมาชิก",
"welcome": "ยินดีต้อนรับ! กรุณากรอกข้อมูลเพื่อสร้างบัญชี",
"submitButton": "สร้างบัญชีใหม่",
"hasAccount": "มีบัญชีผู้ใช้อยู่แล้ว?",
"loginNow": "เข้าสู่ระบบ",
"successMessage": "สมัครสมาชิกสำเร็จ! กรุณาเข้าสู่ระบบ",
"failMessage": "การสมัครสมาชิกไม่สำเร็จ"
},
"form": {
"username": "ชื่อผู้ใช้",
"email": "อีเมล",
"password": "รหัสผ่าน",
"confirmPassword": "ยืนยันรหัสผ่าน",
"prefix": "คำนำหน้าชื่อ",
"firstName": "ชื่อจริง",
"lastName": "นามสกุล",
"phone": "เบอร์โทรศัพท์",
"prefixOptions": {
"mr": "นาย",
"mrs": "นาง",
"ms": "นางสาว"
},
"placeholders": {
"email": "student{'@'}example.com",
"password": "••••••••",
"createPassword": "ตั้งรหัสผ่าน (ขั้นต่ำ 8 ตัวอักษร)",
"confirmPassword": "ยืนยันรหัสผ่านอีกครั้ง",
"username": "username"
}
},
"validation": {
"required": "กรุณากรอกข้อมูลในช่องนี้",
"emailInvalid": "รูปแบบอีเมลไม่ถูกต้อง",
"passwordLength": "รหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร",
"noThai": "ไม่อนุญาตให้ใช้ภาษาไทย",
"noNumber": "ไม่อนุญาตให้ใส่ตัวเลข",
"onlyNumber": "กรุณากรอกเฉพาะตัวเลขเท่านั้น",
"passwordMismatch": "รหัสผ่านไม่ตรงกัน",
"loginFailed": "เข้าสู่ระบบไม่สำเร็จ กรุณาตรวจสอบอีเมลหรือรหัสผ่าน"
},
"backToHome": "กลับสู่หน้าหลัก"
},
"landing": {
"header": {
"menu": {
"allCourses": "คอร์สทั้งหมด",
"discovery": "ค้นหาคอร์ส"
},
"actions": {
"login": "เข้าสู่ระบบ",
"getStarted": "เริ่มต้นใช้งาน",
"enterDashboard": "เข้าสู่แดชบอร์ด"
}
},
"hero": {
"badge": "🚀 เริ่มต้นเส้นทางความสำเร็จใหม่ของคุณที่นี่",
"titlePart1": "ยกระดับ",
"titleHighlight": "ทักษะแห่งอนาคต",
"titlePart2": "ไปกับเรา",
"subtitle": "แหล่งรวมความรู้ออนไลน์ที่เข้าถึงง่ายที่สุด พัฒนาโดยผู้เชี่ยวชาญเพื่อให้คุณก้าวไปสู่เป้าหมายได้อย่างมั่นใจ",
"ctaStart": "เริ่มเรียนฟรี",
"ctaBrowse": "ดูคอร์สทั้งหมด"
},
"features": {
"certificate": {
"title": "รับใบประกาศนียบัตร",
"desc": "เมื่อเรียนจบคอร์ส"
},
"anywhere": {
"title": "เรียนได้ทุกที่",
"desc": "รองรับทุกอุปกรณ์"
}
},
"whyChooseUs": {
"badge": "ทำไมต้องเลือกเรา",
"title": "ออกแบบมาเพื่อความสำเร็จของคุณ",
"desc": "เราไม่ได้เป็นเพียงแค่แพลตฟอร์มเรียนออนไลน์ แต่เป็นพาร์ทเนอร์ที่จะช่วยผลักดันให้คุณไปถึงเป้าหมาย",
"items": {
"material": {
"title": "เนื้อหาคุณภาพสูง",
"desc": "วิดีโอคมชัด พร้อมเอกสารประกอบการเรียนที่คัดสรรมาอย่างดี เข้าใจง่าย"
},
"assessment": {
"title": "วัดผลแม่นยำ",
"desc": "ระบบแบบทดสอบออนไลน์ (Quiz) รู้ผลทันที พร้อมวิเคราะห์จุดที่ควรปรับปรุง"
},
"tracking": {
"title": "ติดตามผลได้ตลอด",
"desc": "ดูความคืบหน้าการเรียนได้ทุกที่ทุกเวลา ผ่าน Dashboard ที่สรุปทุกอย่างให้อย่างลงตัว"
}
}
},
"footer": {
"brandDesc": "แพลตฟอร์มการเรียนรู้ออนไลน์สำหรับทักษะแห่งอนาคต",
"services": {
"title": "บริการทั้งหมด",
"onlineCourses": "คอร์สออนไลน์",
"onsite": "คอร์สเรียนสด (Onsite)",
"classSystem": "ระบบห้องเรียน",
"forOrg": "สำหรับองค์กร"
},
"about": {
"title": "เกี่ยวกับเรา",
"yearlyPackage": "แพ็กเกจรายปี",
"learningPath": "เส้นทางการเรียนรู้",
"skillCheck": "ประเมินทักษะ",
"articles": "บทความ",
"faq": "คำถามที่พบบ่อย"
},
"join": {
"title": "ร่วมงานกับเรา",
"jobs": "สมัครงาน",
"affiliate": "โปรแกรมพันธมิตร",
"instructor": "สมัครเป็นผู้สอน",
"aboutUs": "เกี่ยวกับเรา",
"contact": "ติดต่อเรา"
},
"download": {
"title": "ดาวน์โหลดแอปพลิเคชัน"
},
"consult": {
"title": "ปรึกษาหลักสูตร",
"addLine": "แอดไลน์เพื่อน"
},
"legal": {
"terms": "เงื่อนไขการใช้งาน",
"privacy": "นโยบายความเป็นส่วนตัว",
"refund": "นโยบายการคืนเงิน",
"copyright": "© สงวนลิขสิทธิ์ 2019-2026 บริษัท ไลค์ มี เอ็กซ์ จำกัด"
}
}
}
}

View file

@ -1,48 +1,82 @@
// Nuxt 3 + Quasar + Tailwind + TypeScript
// Configuration for E-Learning Platform
export default defineNuxtConfig({
modules: ["nuxt-quasar-ui", "@nuxtjs/tailwindcss"],
css: ["~/assets/css/main.css"],
typescript: {
strict: true,
compatibilityDate: '2026-01-19',
modules: [
'nuxt-quasar-ui',
'@nuxtjs/tailwindcss',
'@nuxtjs/i18n'
],
i18n: {
strategy: 'no_prefix',
defaultLocale: 'th',
// ✅ สำคัญ: ไม่ใส่ i18n/ ซ้ำ (ฐานคือโฟลเดอร์ i18n อยู่แล้ว)
langDir: 'locales',
lazy: true,
locales: [
{ code: 'th', iso: 'th-TH', name: 'ไทย', file: 'th.json' },
{ code: 'en', iso: 'en-US', name: 'English', file: 'en.json' }
],
// ✅ ให้ใช้ config จากไฟล์นี้ด้วย (ถ้าคุณมี i18n.config.ts)
vueI18n: './i18n.config.ts',
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root'
}
},
css: ['~/assets/css/main.css'],
typescript: {
strict: true
},
quasar: {
plugins: ["Notify"],
plugins: ['Notify'],
config: {
brand: {
primary: "#4b82f7",
secondary: "#2f5ed7",
accent: "#44d4a8",
dark: "#0f1827",
},
},
primary: '#4b82f7',
secondary: '#2f5ed7',
accent: '#44d4a8',
dark: '#0f1827'
}
}
},
components: [
{
path: "~/components",
pathPrefix: false,
},
path: '~/components',
pathPrefix: false
}
],
app: {
head: {
htmlAttrs: {
lang: 'th',
lang: 'th'
},
title: "E-Learning System",
meta: [
{ name: "viewport", content: "width=device-width, initial-scale=1" },
],
title: 'E-Learning System',
meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }],
link: [
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Prompt:wght@300;400;500;600;700;800;900&family=Sarabun:wght@300;400;500;600;700;800&display=swap",
},
],
},
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Prompt:wght@300;400;500;600;700;800;900&family=Sarabun:wght@300;400;500;600;700;800&display=swap'
}
]
}
},
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
}
}
});
})

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#a)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600"><rect width="900" height="600" fill="#A51931"/><rect width="900" height="400" y="100" fill="#F4F5F8"/><rect width="900" height="200" y="200" fill="#2D2A4A"/></svg>

After

Width:  |  Height:  |  Size: 226 B