diff --git a/app/package.json b/app/package.json index c44e96f..642ad6b 100644 --- a/app/package.json +++ b/app/package.json @@ -26,6 +26,7 @@ "socket.io-client": "^4.7.1", "validator": "^13.11.0", "vue": "^3.3.4", + "vue-i18n": "11.3.0", "vue-router": "^4.2.4", "vue-toastification": "^2.0.0-rc.5", "vue3-lottie": "^3.2.0", diff --git a/app/src/components/AuthForm.vue b/app/src/components/AuthForm.vue index 1f658af..36a7eba 100644 --- a/app/src/components/AuthForm.vue +++ b/app/src/components/AuthForm.vue @@ -1,10 +1,13 @@ + diff --git a/app/src/components/game/CompleteModal.vue b/app/src/components/game/CompleteModal.vue index 996928b..9dc5170 100644 --- a/app/src/components/game/CompleteModal.vue +++ b/app/src/components/game/CompleteModal.vue @@ -1,4 +1,5 @@ diff --git a/app/src/components/game/DescriptionPart.vue b/app/src/components/game/DescriptionPart.vue index 5ebd797..1a29012 100644 --- a/app/src/components/game/DescriptionPart.vue +++ b/app/src/components/game/DescriptionPart.vue @@ -1,8 +1,11 @@ diff --git a/app/src/components/header/LanguageSelector.vue b/app/src/components/header/LanguageSelector.vue new file mode 100644 index 0000000..5b2ad59 --- /dev/null +++ b/app/src/components/header/LanguageSelector.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts new file mode 100644 index 0000000..4a9cc80 --- /dev/null +++ b/app/src/i18n/index.ts @@ -0,0 +1,41 @@ +import { createI18n } from 'vue-i18n' +import en from './locales/en.json' +import zhTW from './locales/zh-TW.json' + +export const supportedLocales = ['zh-TW', 'en'] as const + +export type SupportedLocale = (typeof supportedLocales)[number] + +function isSupportedLocale(locale: string): locale is SupportedLocale { + return supportedLocales.includes(locale as SupportedLocale) +} + +function getInitialLocale(): SupportedLocale { + const savedLocale = localStorage.getItem('locale') + + if (savedLocale && isSupportedLocale(savedLocale)) { + return savedLocale + } + + return navigator.language.startsWith('zh') ? 'zh-TW' : 'en' +} + +const initialLocale = getInitialLocale() + +document.documentElement.lang = initialLocale + +export const i18n = createI18n({ + legacy: false, + locale: initialLocale, + fallbackLocale: 'zh-TW', + messages: { + 'zh-TW': zhTW, + en, + }, +}) + +export function setLocale(locale: SupportedLocale) { + i18n.global.locale.value = locale + localStorage.setItem('locale', locale) + document.documentElement.lang = locale +} diff --git a/app/src/i18n/locales/en.json b/app/src/i18n/locales/en.json new file mode 100644 index 0000000..2c548e3 --- /dev/null +++ b/app/src/i18n/locales/en.json @@ -0,0 +1,107 @@ +{ + "language": { + "change": "Change language", + "traditional_chinese": "繁體中文", + "english": "English" + }, + "landing_nav": { + "intro": "Introduction", + "features": "Features", + "about": "About", + "contact": "Contact", + "login": "Sign in", + "report_bug": "Report a bug" + }, + "landing": { + "intro": "Programming is about more than writing code - it also means knowing how to interact effectively with your computer. Yet programming education often overlooks the importance of the development environment, leaving many learners frustrated. A development environment includes all the tools needed to write and run software, many of which are used through terminal commands. While most learners use Windows or macOS, much of the software world runs on Linux. Linux Odyssey is an interactive online learning platform designed to bridge that gap.", + "feature1": "Easy for Beginners", + "feature1_desc": "Setting up and using development tools can be a major hurdle for beginners. With Linux Odyssey, there is nothing to install - learners can access a real Linux environment directly from their browser.", + "feature2": "Learn Through Play", + "feature2_desc": "Linux Odyssey combines the fun of gaming with practical learning. Through carefully designed levels, learners take on challenges, solve puzzles, and master Linux commands along the way.", + "feature3": "Hands-on Linux Experience", + "feature3_desc": "Linux Odyssey goes beyond theory by providing real hands-on experience. Every command runs in a real Linux environment, giving learners an authentic introduction to the world of software development.", + "start_journey": "Grab your keyboard and begin your Linux adventure!", + "team_intro": "Meet the Team", + "team_desc": "NTUT Intelligent Automation Engineering, 2nd Cohort", + "team_member1": "Wancat", + "team_member2": "Neil", + "team_member3": "A.H.", + "team_member4": "Ray Gur", + "awards": "Awards", + "award_desc": "2023 InnoServe Awards - 2nd Place, Titansoft Agile Development Award", + "player_feedback": "What Players Say", + "feedback1": "\"It's really fun - just like a puzzle game.\"", + "feedback2": "\"I never knew free software existed. Thank you!\"", + "feedback3": "\"It's pretty fun. It makes programming enjoyable.\"", + "feedback4": "\"I used to dislike computer class, but now I like it a little more.\"", + "feedback5": "\"The program is really beginner-friendly, and I learned a lot.\"", + "feedback6": "\"It's very well made, clear, and easy to understand. A lot of thought went into the level design.\"", + "feedback7": "\"I found the experience really interesting. I got to try programming in a way I never had before, and the student mentors were very helpful. Thank you for explaining everything so carefully. I hope I can join an activity like this again.\"", + "feedback8": "\"I used to think programming was difficult, but now I find it interesting.\"", + "feedback9": "\"I used to think programming was difficult and had nothing to do with my life. After this class, it feels much more approachable.\"", + "contact_us": "Contact Us" + }, + "authform": { + "google_login": "Continue with Google", + "github_login": "Continue with GitHub", + "or": "or", + "username": "Username", + "username_rules": "At most 32 characters, lowercase first, use lowercase letters, numbers, _, and -", + "email": "Email", + "password": "Password", + "login": "Login", + "register": "Register", + "already_registered": "Already have an account?", + "not_registered": "Don't have an account?", + "continue_as_guest": "Continue as Guest", + "password_rules": "At least 8 characters, including uppercase and lowercase letters and numbers" + }, + "authform_error": { + "too_many_requests": "Too many requests, please try again in 2 minutes", + "unauthorized": "Invalid username or password", + "auth_failed": "Something went wrong", + "invalid_username": "Invalid username", + "invalid_email": "Invalid email", + "invalid_password": "Invalid password", + "username_taken": "Username is already taken", + "email_taken": "Email is already taken" + }, + "game_nav": { + "survey": "Survey", + "leaderboard": "Leaderboard", + "map": "Map", + "bug_report": "Bug Report", + "sign_out": "Sign Out", + "sign_out_failed": "Sign Out Failed", + "load_user_profile_failed": "Failed to Load User Profile" + }, + "quest_map": { + "title": "Quest Map", + "intro": "Embark on your Linux adventure!", + "load_quest_map_failed": "Failed to Load Quest Map" + }, + "quest": { + "start": "Start Adventure", + "locked": "Quest not unlocked" + }, + "leaderboard": { + "title": "Leaderboard", + "username": "Username", + "score": "Score", + "completed_quests": "Completed Quests", + "load_leaderboard_failed": "Failed to Load Leaderboard" + }, + "game": { + "start": "Start", + "task": "Task:", + "loading": "Loading...", + "description": "Description", + "hint": "Hint", + "terminal": "Terminal", + "open_terminal_in_new_tab": "Open Terminal in New Tab", + "reset": "Reset", + "continue": "Continue", + "command_book": "Command Book", + "cheatsheets": "Cheat Sheets" + } +} diff --git a/app/src/i18n/locales/zh-TW.json b/app/src/i18n/locales/zh-TW.json new file mode 100644 index 0000000..43a6f17 --- /dev/null +++ b/app/src/i18n/locales/zh-TW.json @@ -0,0 +1,107 @@ +{ + "language": { + "change": "切換語言", + "traditional_chinese": "繁體中文", + "english": "English" + }, + "landing_nav": { + "intro": "前言", + "features": "特色", + "about": "關於", + "contact": "聯絡我們", + "login": "登入", + "report_bug": "回報錯誤" + }, + "landing": { + "intro": "程式設計不僅僅是寫程式碼,還涉及到如何有效地與你的電腦互動。程式教育卻往往忽略背後「開發環境」的重要性,使得許多學生在過程中充滿挫折。「開發環境」是寫程式、執行程式過程所需要的一切工具。而這些工具需要在終端機輸入指令來使用。大部分的程式要用來運行的環境,並非 Windows 或 Mac OS,而是 Linux 作業系統。 Linux Odyssey 即是為此設計的線上 Linux 互動教學網站。", + "feature1": "初學者也能立即上手", + "feature1_desc": "我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux 環境。", + "feature2": "遊戲般的學習體驗", + "feature2_desc": "Linux Odyssey 融合了遊戲的樂趣與學習的實用性。透過精心設計的遊戲關卡,學習者可以一邊接受挑戰、破解謎題,一邊掌握 Linux 指令的運用。", + "feature3": "真實的 Linux 操作之旅", + "feature3_desc": "Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。每一個指令都是在真實的 Linux 環境中執行,讓學習者能夠真正深入軟體開發的世界。", + "start_journey": "準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!", + "team_intro": "團隊介紹", + "team_desc": "國立臺北科技大學 智慧自動化工程科 第二屆", + "team_member1": "Wancat", + "team_member2": "Neil", + "team_member3": "A.H.", + "team_member4": "Ray Gur", + "awards": "獲獎紀錄", + "award_desc": "2023大專校院資訊應用服務創新競賽 鈦坦敏捷開發特別獎 第二名", + "player_feedback": "玩家感想", + "feedback1": "\"很好玩很像益智遊戲\"", + "feedback2": "\"我從來不知道自由軟體的存在,謝謝你們。\"", + "feedback3": "\"還蠻好玩的,讓程式變好玩\"", + "feedback4": "\"以前不喜歡電腦課現在覺得喜歡一點點\"", + "feedback5": "\"程式真的對新手很友善,讓我學到了很多東西\"", + "feedback6": "\"感覺做得很好很簡明易懂關卡設計很用心\"", + "feedback7": "\"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動\"", + "feedback8": "\"以前我覺得寫程式很難不過現在覺得很有趣\"", + "feedback9": "\"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近\"", + "contact_us": "聯絡我們" + }, + "authform": { + "google_login": "以 Google 繼續", + "github_login": "以 GitHub 繼續", + "or": "或", + "username": "帳號名稱", + "username_rules": "至多 32 字元,小寫字母開頭,可用小寫字母、數字、_ 和 -", + "email": "電子郵件", + "password": "密碼", + "login": "登入", + "register": "註冊", + "already_registered": "已經有帳號了嗎?", + "not_registered": "還沒有帳號嗎?", + "continue_as_guest": "以訪客身分繼續", + "password_rules": "至少 8 個字元,包含數字及大小寫字母" + }, + "authform_error": { + "too_many_requests": "太多請求,兩分鐘後再試一次", + "unauthorized": "無效的帳號名稱或密碼", + "auth_failed": "出了點問題", + "invalid_username": "無效的帳號名稱", + "invalid_email": "無效的電子郵件", + "invalid_password": "無效的密碼", + "username_taken": "帳號名稱已被使用", + "email_taken": "電子郵件已被使用" + }, + "game_nav": { + "survey": "問卷", + "leaderboard": "排行榜", + "map": "地圖", + "bug_report": "回報錯誤", + "sign_out": "登出", + "sign_out_failed": "登出失敗", + "load_user_profile_failed": "無法讀取使用者資料" + }, + "quest_map": { + "title": "關卡地圖", + "intro": "踏上你的Linux冒險之旅吧!", + "load_quest_map_failed": "無法讀取關卡地圖" + }, + "quest": { + "start": "開始冒險", + "locked": "關卡尚未解鎖" + }, + "leaderboard": { + "title": "排行榜", + "username": "使用者名稱", + "score": "分數", + "completed_quests": "完成關卡", + "load_leaderboard_failed": "無法讀取排行榜" + }, + "game": { + "start": "開始", + "task": "任務:", + "loading": "載入中...", + "description": "說明", + "hint": "提示", + "terminal": "終端機", + "open_terminal_in_new_tab": "在新分頁開啟終端機", + "reset": "重置", + "continue": "繼續", + "command_book": "指令之書", + "cheatsheets": "指令列表" + } +} diff --git a/app/src/main.ts b/app/src/main.ts index 6e918ff..a0a869d 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -1,18 +1,15 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' - -import './style.css' -/* import font awesome icon component */ -import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' -import './library' -/* import Vue Toastification */ -import Toast, { POSITION } from 'vue-toastification' -/* import Vue Lottie */ -import Vue3Lottie from 'vue3-lottie' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // import font awesome icon component +import { i18n } from './i18n' +import Toast, { POSITION } from 'vue-toastification' // import Vue Toastification +import Vue3Lottie from 'vue3-lottie' // import Vue Lottie import 'vue-toastification/dist/index.css' import App from './App.vue' import router from './router' import formbricks from './utils/formbricks' +import './style.css' +import './library' const app = createApp(App) const pinia = createPinia() @@ -21,6 +18,7 @@ app.component('font-awesome-icon', FontAwesomeIcon) app.use(pinia) app.use(router) +app.use(i18n) router.afterEach(() => { if (typeof formbricks !== 'undefined') { diff --git a/app/src/utils/auth.ts b/app/src/utils/auth.ts index 4b7e058..c79be1c 100644 --- a/app/src/utils/auth.ts +++ b/app/src/utils/auth.ts @@ -1,4 +1,5 @@ import api from './api' +import { i18n } from '../i18n' import { TooManyRequestsError, @@ -32,11 +33,15 @@ function handleRequestError(err: { const { path, msg } = errors[0] switch (path) { case 'username': - throw new ValidationError('Username is already taken') + throw new ValidationError( + i18n.global.t('authform_error.username_taken') + ) case 'email': - throw new ValidationError('Email is already taken') + throw new ValidationError(i18n.global.t('authform_error.email_taken')) case 'password': - throw new ValidationError('Password is too weak') + throw new ValidationError( + i18n.global.t('authform_error.invalid_password') + ) default: throw new ValidationError(msg) } diff --git a/app/src/views/LandingPage.vue b/app/src/views/LandingPage.vue index 3b98d6a..bf84fad 100644 --- a/app/src/views/LandingPage.vue +++ b/app/src/views/LandingPage.vue @@ -1,10 +1,13 @@