feat: implement multilingual feature for UI (#202)

This commit is contained in:
Justin Lin authored and GitHub committed 2026-09-26 10:57:20 +10:00
commit cf5e8a1f24
27 files changed
+569 -150

No files matched your search

+1
View File
@@ -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",
+31 -19
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import api from '../utils/api'
const { t } = useI18n()
const emit = defineEmits(['onSubmit', 'onChange'])
defineProps({
type: {
type: String,
@@ -124,7 +127,7 @@ const registerGuest = async () => {
:icon="['fab', 'google']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium"> 以 Google 繼續 </span>
<span class="font-medium"> {{ t('authform.google_login') }} </span>
</a>
<a
id="GitHubLogin"
@@ -136,14 +139,14 @@ const registerGuest = async () => {
:icon="['fab', 'github']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium"> 以 GitHub 繼續 </span>
<span class="font-medium"> {{ t('authform.github_login') }} </span>
</a>
<p
id="or"
class="text-text-secondary flex justify-center mt-3"
v-if="type === 'login' || type === 'register'"
>
或
{{ t('authform.or') }}
</p>
</div>
<div class="mb-6">
@@ -152,20 +155,22 @@ const registerGuest = async () => {
id="username"
ref="usernameInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
:placeholder="type === 'login' ? '電子郵件 / 帳號名稱' : '帳號名稱'"
:placeholder="
t('authform.username') +
(type === 'login' ? ' / ' + t('authform.email') : '')
"
v-model="username"
required
@input="handleChange()"
autocomplete="username"
/>
<label
class="text-sm font-normal text-text-secondary"
<p
class="mx-1 text-xs font-normal text-text-secondary"
id="NameRules"
v-if="type === 'username' || type === 'register'"
v-if="type === 'register'"
>
<p>可用小寫字母、數字、"_"和"-"</p>
<p>小寫字母開頭,至多32字元</p>
</label>
{{ t('authform.username_rules') }}
</p>
</div>
<div class="mb-6" v-if="type === 'register'">
<input
@@ -173,7 +178,7 @@ const registerGuest = async () => {
id="email"
ref="emailInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="電子郵件"
:placeholder="t('authform.email')"
v-model="email"
required
@input="handleChange()"
@@ -185,11 +190,18 @@ const registerGuest = async () => {
type="password"
id="password"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="密碼"
:placeholder="t('authform.password')"
v-model="password"
required
@input="handleChange()"
/>
<p
class="mx-1 text-xs font-normal text-text-secondary"
id="PasswordRules"
v-if="type === 'register'"
>
{{ t('authform.password_rules') }}
</p>
</div>
<p
id="ErrorDisplay"
@@ -203,29 +215,29 @@ const registerGuest = async () => {
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
type="submit"
>
<span v-if="type === 'login'"> 登入 </span>
<span v-else> 註冊 </span>
<span v-if="type === 'login'"> {{ t('authform.login') }} </span>
<span v-else> {{ t('authform.register') }} </span>
</button>
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
<span id="CheckRegistered" v-if="type === 'register'">
已經有帳號了嗎?
{{ t('authform.already_registered') }}
<RouterLink
id="SwitchtoLogin"
class="text-text-primary font-bold"
to="/login"
>
<u> 登入 </u>
<u> {{ t('authform.login') }} </u>
</RouterLink></span
>
<span id="CheckRegistered" v-else-if="type === 'login'">
還沒有帳號嗎?
{{ t('authform.not_registered') }}
<RouterLink
id="SwitchtoRegister"
class="text-text-primary font-bold"
to="/register"
>
<u> 註冊 </u>
<u> {{ t('authform.register') }} </u>
</RouterLink></span
>
</p>
@@ -234,7 +246,7 @@ const registerGuest = async () => {
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
@click="registerGuest()"
>
以訪客身分繼續
{{ t('authform.continue_as_guest') }}
</button>
</form>
</div>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref, defineProps, onMounted } from 'vue'
import MarkdownText from './MarkdownText.vue'
import { trpc } from '../utils/trpc'
const { t } = useI18n()
const props = defineProps({
questTitle: {
type: String,
@@ -36,7 +39,7 @@ const handleQuests = (id: string) => {
if (questUnlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
toast.warning(t('quest.locked'))
}
}
const handleColour = () => {
@@ -61,7 +64,6 @@ onMounted(async () => {
<span class="close text-text content-between" @click="closeModal"
>&times;</span
>
<h2
id="QuestTitle"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
@@ -82,8 +84,9 @@ onMounted(async () => {
@click="handleQuests(questId)"
:style="{ backgroundColor: questColor, color: questTextColor }"
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
>開始冒險</a
>
{{ t('quest.start') }}
</a>
</div>
</div>
</template>
+3 -1
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref, onMounted, watch, computed } from 'vue'
import { useToast } from 'vue-toastification'
import { DAG } from '../../../packages/utils'
@@ -6,6 +7,7 @@ import { trpc } from '../utils/trpc'
import useUserProfile from '../store/userProfile'
import QuestIntro from './QuestIntro.vue'
const { t } = useI18n()
const store = useUserProfile()
const toast = useToast()
@@ -151,7 +153,7 @@ const edgeStyle = computed(() => {
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
>
踏上你的Linux冒險之旅吧!
{{ t('quest_map.intro') }}
</h1>
<svg
:width="svgWidth"
+10 -2
View File
@@ -1,7 +1,15 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
<h1 id="CommandBook" class="inline text-text-primary font-xl">
{{ t('game.command_book') }}
</h1>
<div id="cheatsheets">
<p class="text-text-secondary">-- --指令列表-- --</p>
<p class="text-text-secondary">-- --{{ t('game.cheatsheets') }}-- --</p>
</div>
</template>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref } from 'vue'
@@ -6,6 +7,8 @@ import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config'
import CompleteAnimation from '../../img/complete-animation.json'
const { t } = useI18n()
const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
+25 -8
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { useSession } from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
const { t } = useI18n()
const router = useRouter()
const sessionStore = useSession()
const reset = async () => {
@@ -14,16 +17,17 @@ const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
useToast().error('無法讀取地圖')
useToast().error(t('quest_map.load_quest_map_failed'))
console.error(err)
}
}
</script>
<template>
<section class="mt-2 flex flex-row gap-3 justify-center">
<section class="mt-2 flex flex-row gap-2 justify-center">
<button
id="survey"
:title="t('game_nav.survey')"
class="bg-bg-secondary rounded-lg p-2"
@click="openQuestSurvey"
>
@@ -31,15 +35,18 @@ const continuePlay = async () => {
:icon="['fas', 'file-invoice']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline md:flex">問卷</p>
<p class="text-text-primary text-xs">{{ t('game_nav.survey') }}</p>
</button>
<button
title="Bug Report"
id="bug-report"
:title="t('game_nav.bug_report')"
@click="openBugReport"
class="bg-bg-secondary rounded-lg p-2"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<p class="text-text-primary inline ml-1 md:flex">回報</p>
<p class="text-text-primary text-xs">
{{ t('game_nav.bug_report') }}
</p>
</button>
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
@@ -48,15 +55,23 @@ const continuePlay = async () => {
/>
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
</button> -->
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<button
id="reset"
:title="t('game.reset')"
class="bg-bg-secondary rounded-lg p-2"
@click="reset"
>
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">重來</p>
<p class="text-text-primary text-xs">
{{ t('game.reset') }}
</p>
</button>
<button
id="continue"
:title="t('game.continue')"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
@@ -64,7 +79,9 @@ const continuePlay = async () => {
:icon="['far', 'circle-right']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
<p class="text-text-primary text-xs">
{{ t('game.continue') }}
</p>
</button>
</section>
</template>
+7 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, ref, watch } from 'vue'
import { useSession } from '../../store/session'
import MarkdownText from '../MarkdownText.vue'
const { t } = useI18n()
const sessionStore = useSession()
const current = ref(-1)
const hintDetails = ref<HTMLDetailsElement | null>(null)
@@ -52,7 +55,9 @@ watch(response, () => {
:icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center"
/>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
{{ t('game.description') }}
</h1>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
@@ -98,7 +103,7 @@ watch(response, () => {
class="text-text-primary font-xl whitespace-pre-wrap"
>
<details ref="hintDetails">
<summary class="cursor-pointer">提示</summary>
<summary class="cursor-pointer">{{ t('game.hint') }}</summary>
<MarkdownText :content="response.hint" />
</details>
</li>
+5 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import MarkdownText from '../MarkdownText.vue'
import { useSession } from '../../store/session'
const { t } = useI18n()
const sessionStore = useSession()
const tasks = computed(() => {
console.log('session', sessionStore.session)
@@ -31,14 +34,14 @@ const tasks = computed(() => {
:content="sessionStore.quest.instruction"
/>
<br />
<p id="tasks" class="text-text">任務:</p>
<p id="tasks" class="text-text font-bold">{{ t('game.task') }}</p>
<ul v-if="sessionStore.session">
<li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" />
</li>
</ul>
</div>
<p class="text-text" v-else>Loading...</p>
<p class="text-text" v-else>{{ t('game.loading') }}</p>
</div>
</div>
</template>
+4 -1
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
import { computed } from 'vue'
import { useSession } from '../../store/session'
const { t } = useI18n()
const toast = useToast()
const sessionStore = useSession()
@@ -31,7 +34,7 @@ const startSession = async () => {
:icon="['fas', 'play']"
class="text-text-primary h-full px-3"
/>
<p class="text-text-primary inline ml-1 md:flex">開始</p>
<p class="text-text-primary inline ml-1 md:flex">{{ t('game.start') }}</p>
</button>
</div>
</template>
+6 -3
View File
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { onMounted, onUnmounted, ref } from 'vue'
import { useSession, useTerminal } from '../../store/session'
const { t } = useI18n()
const sessionStore = useSession()
const terminal = ref<HTMLElement | null>(null)
@@ -24,14 +27,14 @@ onMounted(() => {
<div class="flex flex-col h-full">
<div class="flex items-center gap-2 p-1">
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text" />
<button id="Terminal" class="text-text">終端機</button>
<p id="Terminal" class="text-text">{{ t('game.terminal') }}</p>
<a
class="text-sm font-normal text-gray-200 ml-10"
class="text-xs font-normal text-gray-200 ml-10 bg-bg-primary rounded-lg px-2 py-1 hover:bg-bg-primary-hover transition-colors duration-300"
:href="`/terminal/${sessionStore.session?.containerName}?token=${sessionStore.session?.token}`"
target="_blank"
>
Open Terminal in New Tab
{{ t('game.open_terminal_in_new_tab') }}
</a>
</div>
<div
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
@@ -8,6 +9,7 @@ import { openBugReport } from '../../utils/formbricks'
import { useSession } from '../../store/session'
import useUserProfile from '../../store/userProfile'
const { t } = useI18n()
const userStore = useUserProfile()
const sessionStore = useSession()
const router = useRouter()
@@ -21,7 +23,7 @@ const handleLogout = async () => {
sessionStore.reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('登出失敗')
toast.error(t('game_nav.sign_out_failed'))
console.error(err)
}
}
@@ -35,7 +37,7 @@ onMounted(async () => {
try {
await userStore.loadUserProfile()
} catch (err) {
toast.error('無法讀取使用者資料')
toast.error(t('game_nav.load_user_profile_failed'))
console.error(err)
}
})
@@ -67,7 +69,7 @@ onMounted(async () => {
{{ userStore.username }}
</p>
<button
title="Survey"
:title="t('game_nav.survey')"
target="_blank"
class="md:h-5 md:w-5 px-1.5 w-auto"
@click="openSurvey"
@@ -76,37 +78,36 @@ onMounted(async () => {
:icon="['fas', 'file-invoice']"
class="text-text-primary inline"
/>
<span class="text-text sm:hidden px-2">Survey</span>
</button>
<RouterLink
title="LeaderBoard"
:title="t('game_nav.leaderboard')"
to="/leaderboard"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'trophy']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Leader Board</span>
</RouterLink>
<RouterLink title="Map" to="/map" class="md:h-5 md:w-5 px-1.5 w-auto">
<RouterLink
:title="t('game_nav.map')"
to="/map"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Map</span>
</RouterLink>
<button
title="Bug Report"
:title="t('game_nav.bug_report')"
@click="openBugReport"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Bug Report</span>
</button>
<button
title="Sign Out"
:title="t('game_nav.sign_out')"
@click="handleLogout"
class="md:h-5 md:w-5 px-1.5 w-auto"
class="md:h-5 md:w-5 px-1.5 mr-2 w-auto"
>
<font-awesome-icon
:icon="['fas', 'arrow-right-from-bracket']"
class="text-text-primary"
/>
<span class="text-text sm:hidden px-2">Sign Out</span>
</button>
</template>
+8 -2
View File
@@ -1,5 +1,10 @@
<script setup lang="ts">
import { defineProps, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import LanguageSelector from './LanguageSelector.vue'
const { t } = useI18n()
defineProps({
title: {
@@ -19,7 +24,7 @@ const toggleMenu = () => {
</script>
<template>
<div
class="w-full top-0 px-5 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
class="w-full top-0 px-2 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
>
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
@@ -44,6 +49,7 @@ const toggleMenu = () => {
:class="{ hidden: !menuOpen }"
>
<component :is="headerComponent" />
<LanguageSelector />
</div>
<div
v-if="windowWidth"
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { bugReportUrl } from '../../config'
const { t } = useI18n()
const scrollToSection = (sectionId: string) => {
const section = document.getElementById(sectionId)
if (section) {
@@ -21,22 +24,25 @@ const scrollToSection = (sectionId: string) => {
@click="scrollToSection('intro')"
style="font-size: 2vh"
>
前言
{{ t('landing_nav.intro') }}
</button>
<button
id="Feature"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('feature')"
style="font-size: 2vh"
>
特色</button
><button
{{ t('landing_nav.features') }}
</button>
<button
id="Info"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('about')"
style="font-size: 2vh"
>
關於
{{ t('landing_nav.about') }}
</button>
<button
@@ -45,18 +51,27 @@ const scrollToSection = (sectionId: string) => {
@click="scrollToSection('contact')"
style="font-size: 2vh"
>
聯絡我們
{{ t('landing_nav.contact') }}
</button>
<RouterLink
title="Sign In"
id="SignIn"
to="/register"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
style="font-size: 2vh"
>
登入
{{ t('landing_nav.login') }}
</RouterLink>
<a title="Bug Report" :href="bugReportUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<a
:title="t('landing_nav.report_bug')"
:href="bugReportUrl"
target="_blank"
class="h-5 w-5"
>
<font-awesome-icon
:icon="['fas', 'bug']"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
/>
</a>
</template>
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { setLocale, type SupportedLocale } from '../../i18n'
const { locale } = useI18n({ useScope: 'global' })
function changeLanguage(event: Event) {
const selectedLocale = (event.target as HTMLSelectElement)
.value as SupportedLocale
setLocale(selectedLocale)
}
</script>
<template>
<select
:value="locale"
class="bg-bg-secondary px-3 py-2 text-sm text-text shadow-md outline-none"
aria-label="Language"
@change="changeLanguage"
style="font-size: 2vh"
>
<option value="en">English</option>
<option value="zh-TW">繁體中文</option>
</select>
</template>
+41
View File
@@ -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
}
+107
View File
@@ -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"
}
}
+107
View File
@@ -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": "指令列表"
}
}
+7 -9
View File
@@ -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') {
+8 -3
View File
@@ -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)
}
+43 -53
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import ContactButton from '../components/ContactButton.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import LandingHeaderComponents from '../components/header/LandingHeaderComponents.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import { facebookUrl, githubUrl } from '../config'
const { t } = useI18n()
</script>
<template>
<div class="w-screen h-screen bg-scroll bg-bg">
@@ -18,13 +21,9 @@ import { facebookUrl, githubUrl } from '../config'
<div
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
>
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
Linux
</h1>
<h1 class="text-text-primary text-7xl font-extrabold">Linux</h1>
<img src="../img/icon_totem.svg" class="h-[30vh]" />
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
Odyssey
</h1>
<h1 class="text-text-primary text-7xl font-extrabold">Odyssey</h1>
</div>
<!-- intro -->
<div class="w-full md:w-2/3 px-5">
@@ -36,13 +35,7 @@ import { facebookUrl, githubUrl } from '../config'
class="text-text inline-block bg-bg-primary p-5 rounded-xl opacity-90"
style="font-size: 2.5vh"
>
程式設計不僅僅是寫程式碼,還涉及到如何有效地與你的電腦互動。
程式教育卻往往忽略背後「開發環境」的重要性,
使得許多學生在過程中充滿挫折。
「開發環境」是寫程式、執行程式過程所需要的一切工具。
而這些工具需要在終端機輸入指令來使用。
大部分的程式要用來運行的環境,並非 Windows 或 Mac OS,而是 Linux
作業系統。 Linux Odyssey 即是為此設計的線上 Linux 互動教學網站。
{{ t('landing.intro') }}
</h1>
</section>
<section
@@ -57,31 +50,27 @@ import { facebookUrl, githubUrl } from '../config'
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
class="text-text-primary p-5 font-extrabold col-span-2"
style="font-size: 3.5vh"
>
初學者也能立即上手
{{ t('landing.feature1') }}
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。
學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux
環境。
{{ t('landing.feature1_desc') }}
</h1>
</div>
<div
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
class="text-text-primary p-5 font-extrabold col-span-2"
style="font-size: 3.5vh"
>
遊戲般的學習體驗
{{ t('landing.feature2') }}
</h1>
<img src="../img/icon_sword.svg" class="h-[18vh] row-span-2" />
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
Linux Odyssey 融合了遊戲的樂趣與學習的實用性。
透過精心設計的遊戲關卡,學習者可以一邊接受挑戰、破解謎題,
一邊掌握 Linux 指令的運用。
{{ t('landing.feature2_desc') }}
</h1>
</div>
<div
@@ -92,15 +81,13 @@ import { facebookUrl, githubUrl } from '../config'
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2 pt-8"
class="text-text-primary p-5 font-extrabold col-span-2 pt-8"
style="font-size: 3.5vh"
>
真實的 Linux 操作之旅
{{ t('landing.feature3') }}
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。
每一個指令都是在真實的 Linux 環境中執行,
讓學習者能夠真正深入軟體開發的世界。
{{ t('landing.feature3_desc') }}
</h1>
</div>
</section>
@@ -120,7 +107,7 @@ import { facebookUrl, githubUrl } from '../config'
>
<RouterLink title="StartGame" id="StartGame" to="/register">
<div class="text-text text-xl p-2 w-full text-center">
準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!
{{ t('landing.start_journey') }}
</div>
</RouterLink>
</section>
@@ -129,24 +116,25 @@ import { facebookUrl, githubUrl } from '../config'
class="text-text flex flex-col items-center mt-10 gap-10"
>
<div class="w-full flex flex-col items-center gap-2">
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
團隊介紹
<h1 class="text-text-primary text-3xl font-extrabold">
{{ t('landing.team_intro') }}
</h1>
<p class="text-lg">國立臺北科技大學 智慧自動化工程科 第二屆</p>
<p class="text-lg">
{{ t('landing.team_desc') }}
</p>
<div class="flex flex-row gap-5 justify-center">
<h1 class="font-bold">Wancat</h1>
<h1 class="font-bold">Neil</h1>
<h1 class="font-bold">A.H.</h1>
<h1 class="font-bold">Ray Gur</h1>
<h1 class="font-bold">{{ t('landing.team_member1') }}</h1>
<h1 class="font-bold">{{ t('landing.team_member2') }}</h1>
<h1 class="font-bold">{{ t('landing.team_member3') }}</h1>
<h1 class="font-bold">{{ t('landing.team_member4') }}</h1>
</div>
</div>
<div class="w-full flex flex-col items-center gap-2">
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
得獎肯定
<h1 class="text-text-primary text-3xl font-extrabold">
{{ t('landing.awards') }}
</h1>
<h1 class="text-xl text-center">
大專校院資訊應用服務創新競賽<br />
鈦坦敏捷開發特別獎 第二名
{{ t('landing.award_desc') }}
</h1>
</div>
<div class="container mx-auto">
@@ -154,35 +142,37 @@ import { facebookUrl, githubUrl } from '../config'
class="justify-items-center grid grid-cols-1 sm:grid-cols-3 gap-5"
>
<h1
class="text-text-primary text-3xl font-extrabold font-mono sm:col-span-3 gap-6"
class="text-text-primary text-3xl font-extrabold sm:col-span-3 gap-6"
>
玩家感想
{{ t('landing.player_feedback') }}
<!--可以做成翻頁卡牌一次一個-->
</h1>
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
<h1 class="text-xl text-center">
"我從來不知道自由軟體的存在,謝謝你們。"
{{ t('landing.feedback1') }}
</h1>
<h1 class="text-xl text-center">
"還蠻好玩的,讓程式變好玩"
{{ t('landing.feedback2') }}
</h1>
<h1 class="text-xl text-center">
"以前不喜歡電腦課現在覺得喜歡一點點"
{{ t('landing.feedback3') }}
</h1>
<h1 class="text-xl text-center">
"程式真的對新手很友善,讓我學到了很多東西"
{{ t('landing.feedback4') }}
</h1>
<h1 class="text-xl text-center">
"感覺做得很好很簡明易懂關卡設計很用心"
{{ t('landing.feedback5') }}
</h1>
<h1 class="text-xl text-center">
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
{{ t('landing.feedback6') }}
</h1>
<h1 class="text-xl text-center">
"以前我覺得寫程式很難不過現在覺得很有趣"
{{ t('landing.feedback7') }}
</h1>
<h1 class="text-xl text-center">
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
{{ t('landing.feedback8') }}
</h1>
<h1 class="text-xl text-center">
{{ t('landing.feedback9') }}
</h1>
</div>
</div>
@@ -192,8 +182,8 @@ import { facebookUrl, githubUrl } from '../config'
id="contact"
class="text-text mt-10 flex flex-col items-center"
>
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
聯絡我們
<h1 class="text-text-primary font-extrabold text-3xl">
{{ t('landing.contact_us') }}
</h1>
<div
class="flex flex-row items-center justify-around mt-5 gap-5 mb-5"
+20 -6
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
@@ -6,6 +7,8 @@ import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import api from '../utils/api'
const { t } = useI18n()
const leaderboard = ref<
{
username: string
@@ -21,7 +24,7 @@ async function getLeaderboard() {
leaderboard.value = res.data
} catch (err: any) {
console.error(err)
toast.error('讀取排行榜失敗')
toast.error(t('leaderboard.load_leaderboard_failed'))
}
}
@@ -37,22 +40,33 @@ onMounted(async () => {
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="w-full">
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
<HeaderPart
:title="t('leaderboard.title')"
:headerComponent="GameHeaderComponents"
/>
</div>
<div class="w-fit h-[94vh] m-3">
<div
class="h-full bg-bg border-8 border-bg-primary flex flex-col items-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl mb-5">排行榜</h1>
<h1 class="text-text-primary text-3xl mb-5">
{{ t('leaderboard.title') }}
</h1>
<div class="max-h-300 overflow-auto">
<table class="w-full text-sm text-left text-text">
<thead
class="text-xs text-text-secondary uppercase bg-bg-secondary"
>
<tr>
<th scope="col" class="px-6 py-3">使用者名稱</th>
<th scope="col" class="px-6 py-3">分數</th>
<th scope="col" class="px-6 py-3">完成關卡</th>
<th scope="col" class="px-6 py-3">
{{ t('leaderboard.username') }}
</th>
<th scope="col" class="px-6 py-3">
{{ t('leaderboard.score') }}
</th>
<th scope="col" class="px-6 py-3">
{{ t('leaderboard.completed_quests') }}
</th>
</tr>
</thead>
<tbody>
+7 -4
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import AuthForm from '../components/AuthForm.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
@@ -10,6 +11,8 @@ import {
} from '../utils/errors'
import { login } from '../utils/auth'
const { t } = useI18n()
const handleLogin = async ({
username,
password,
@@ -27,19 +30,19 @@ const handleLogin = async ({
if (isSuccess) success()
} catch (err) {
if (err instanceof TooManyRequestsError) {
error('太多請求,兩分鐘後再試一次。')
error(t('authform_error.too_many_requests'))
return
}
if (err instanceof UnauthorizedError) {
error('錯誤的帳號名稱或密碼。')
error(t('authform_error.unauthorized'))
return
}
if (err instanceof ValidationError) {
error('無效的帳號名稱或密碼。')
error(err.message)
return
}
console.error(err)
error('出了點問題。')
error(t('authform_error.auth_failed'))
}
}
</script>
+4 -1
View File
@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import QuestMap from '../components/QuestMap.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
const { t } = useI18n()
</script>
<template>
<div class="w-fit">
<HeaderPart
title="關卡地圖"
:title="t('quest_map.title')"
:headerComponent="GameHeaderComponents"
class="z-50"
/>
+11 -8
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import validator from 'validator'
import { isValidUsername } from '../../../packages/utils'
import { passwordPolicy } from '../../../packages/constants'
@@ -12,6 +13,8 @@ import {
ValidationError,
} from '../utils/errors'
const { t } = useI18n()
function handleRegister({
username,
email,
@@ -30,11 +33,11 @@ function handleRegister({
.then(success)
.catch((err) => {
if (err instanceof TooManyRequestsError) {
error('太多請求,兩分鐘後再試一次。')
error(t('authform_error.too_many_requests'))
return
}
if (err instanceof UnauthorizedError) {
error('無效的帳號名稱或密碼。')
error(t('authform_error.unauthorized'))
return
}
if (err instanceof ValidationError) {
@@ -42,7 +45,7 @@ function handleRegister({
return
}
console.error(err)
error('出了點問題。')
error(t('authform_error.auth_failed'))
})
}
@@ -59,15 +62,15 @@ async function check({
error: (msg: string) => void
}) {
if (username && !isValidUsername(username)) {
error('無效的帳號名稱')
error(t('authform_error.invalid_username'))
return
}
if (email && !validator.isEmail(email)) {
error('無效的電子郵件')
error(t('authform_error.invalid_email'))
return
}
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
error(t('authform_error.invalid_password'))
return
}
try {
@@ -78,11 +81,11 @@ async function check({
return
}
if (err instanceof TooManyRequestsError) {
error('太多請求,兩分鐘後再試一次。')
error(t('authform_error.too_many_requests'))
return
}
console.error(err)
error('出了點問題。')
error(t('authform_error.registration_failed'))
}
}
</script>
@@ -26,7 +26,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
res.status(401).json({ message: 'Unauthorized' })
return
}
} catch (error) {
} catch (_error) {
res.status(401).json({ message: 'Unauthorized' })
return
}
+40
View File
@@ -1306,6 +1306,36 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@intlify/core-base@11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.3.0.tgz#55e612501f08b564011cb16d9f4a08e9a05df3cb"
integrity sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==
dependencies:
"@intlify/devtools-types" "11.3.0"
"@intlify/message-compiler" "11.3.0"
"@intlify/shared" "11.3.0"
"@intlify/devtools-types@11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@intlify/devtools-types/-/devtools-types-11.3.0.tgz#2670ac6a786ff56ac7b84f9af472771c7bc954fa"
integrity sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==
dependencies:
"@intlify/core-base" "11.3.0"
"@intlify/shared" "11.3.0"
"@intlify/message-compiler@11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.3.0.tgz#84c5d2af4a2119f836c5f6835b4b88e5d4076d4b"
integrity sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==
dependencies:
"@intlify/shared" "11.3.0"
source-map-js "^1.0.2"
"@intlify/shared@11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.3.0.tgz#7abb22f05e502f3c6bf64dc771c448cdaa81d864"
integrity sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
@@ -9726,6 +9756,16 @@ vue-eslint-parser@^9.4.2:
lodash "^4.17.21"
semver "^7.3.6"
vue-i18n@11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.3.0.tgz#acb92847017f90b0fd96541cb3b08fdef50dc49c"
integrity sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==
dependencies:
"@intlify/core-base" "11.3.0"
"@intlify/devtools-types" "11.3.0"
"@intlify/shared" "11.3.0"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.2.4:
version "4.3.0"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz"