feat(i18n): add localized content for game page

This commit is contained in:
neilchen0812 committed 2026-09-22 02:33:53 +09:30
1 parent 87df12ac85
commit 03fbae616d
9 files changed
+90 -20

No files matched your search

+10 -2
View File
@@ -1,7 +1,15 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template> <template>
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" /> <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"> <div id="cheatsheets">
<p class="text-text-secondary">-- --指令列表-- --</p> <p class="text-text-secondary">-- --{{ t('game.cheatsheets') }}-- --</p>
</div> </div>
</template> </template>
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { ref } from 'vue' import { ref } from 'vue'
@@ -6,6 +7,8 @@ import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config' // import { surveyUrl } from '../config'
import CompleteAnimation from '../../img/complete-animation.json' import CompleteAnimation from '../../img/complete-animation.json'
const { t } = useI18n()
const showModal = ref(true) // Controls whether the modal is visible or not const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
+25 -8
View File
@@ -1,9 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { useSession } from '../../store/session' import { useSession } from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks' import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
const { t } = useI18n()
const router = useRouter() const router = useRouter()
const sessionStore = useSession() const sessionStore = useSession()
const reset = async () => { const reset = async () => {
@@ -14,16 +17,17 @@ const continuePlay = async () => {
try { try {
router.push({ name: 'map' }) router.push({ name: 'map' })
} catch (err) { } catch (err) {
useToast().error('無法讀取地圖') useToast().error(t('quest_map.load_quest_map_failed'))
console.error(err) console.error(err)
} }
} }
</script> </script>
<template> <template>
<section class="mt-2 flex flex-row gap-3 justify-center"> <section class="mt-2 flex flex-row gap-2 justify-center">
<button <button
id="survey" id="survey"
:title="t('game_nav.survey')"
class="bg-bg-secondary rounded-lg p-2" class="bg-bg-secondary rounded-lg p-2"
@click="openQuestSurvey" @click="openQuestSurvey"
> >
@@ -31,15 +35,18 @@ const continuePlay = async () => {
:icon="['fas', 'file-invoice']" :icon="['fas', 'file-invoice']"
class="text-text-primary px-1" 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>
<button <button
title="Bug Report" id="bug-report"
:title="t('game_nav.bug_report')"
@click="openBugReport" @click="openBugReport"
class="bg-bg-secondary rounded-lg p-2" class="bg-bg-secondary rounded-lg p-2"
> >
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" /> <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>
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2"> <!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon <font-awesome-icon
@@ -48,15 +55,23 @@ const continuePlay = async () => {
/> />
<p class="text-text-disabled inline ml-1 md:flex">解答</p> <p class="text-text-disabled inline ml-1 md:flex">解答</p>
</button> --> </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 <font-awesome-icon
:icon="['fas', 'arrow-rotate-left']" :icon="['fas', 'arrow-rotate-left']"
class="text-text-primary px-1" 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>
<button <button
id="continue" id="continue"
:title="t('game.continue')"
class="bg-bg-secondary rounded-lg p-2" class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay" @click="continuePlay"
> >
@@ -64,7 +79,9 @@ const continuePlay = async () => {
:icon="['far', 'circle-right']" :icon="['far', 'circle-right']"
class="text-text-primary px-1" 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> </button>
</section> </section>
</template> </template>
+7 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useSession } from '../../store/session' import { useSession } from '../../store/session'
import MarkdownText from '../MarkdownText.vue' import MarkdownText from '../MarkdownText.vue'
const { t } = useI18n()
const sessionStore = useSession() const sessionStore = useSession()
const current = ref(-1) const current = ref(-1)
const hintDetails = ref<HTMLDetailsElement | null>(null) const hintDetails = ref<HTMLDetailsElement | null>(null)
@@ -52,7 +55,9 @@ watch(response, () => {
:icon="['far', 'lightbulb']" :icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center" 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"> <div v-if="current !== -1" class="flex w-full justify-end items-end">
<button <button
class="p-2 m-1 w-1/8" class="p-2 m-1 w-1/8"
@@ -98,7 +103,7 @@ watch(response, () => {
class="text-text-primary font-xl whitespace-pre-wrap" class="text-text-primary font-xl whitespace-pre-wrap"
> >
<details ref="hintDetails"> <details ref="hintDetails">
<summary class="cursor-pointer">提示</summary> <summary class="cursor-pointer">{{ t('game.hint') }}</summary>
<MarkdownText :content="response.hint" /> <MarkdownText :content="response.hint" />
</details> </details>
</li> </li>
+5 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed } from 'vue' import { computed } from 'vue'
import MarkdownText from '../MarkdownText.vue' import MarkdownText from '../MarkdownText.vue'
import { useSession } from '../../store/session' import { useSession } from '../../store/session'
const { t } = useI18n()
const sessionStore = useSession() const sessionStore = useSession()
const tasks = computed(() => { const tasks = computed(() => {
console.log('session', sessionStore.session) console.log('session', sessionStore.session)
@@ -31,14 +34,14 @@ const tasks = computed(() => {
:content="sessionStore.quest.instruction" :content="sessionStore.quest.instruction"
/> />
<br /> <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"> <ul v-if="sessionStore.session">
<li v-for="task in tasks" :key="task.id"> <li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" /> <MarkdownText :class="task.class" :content="task.content" />
</li> </li>
</ul> </ul>
</div> </div>
<p class="text-text" v-else>Loading...</p> <p class="text-text" v-else>{{ t('game.loading') }}</p>
</div> </div>
</div> </div>
</template> </template>
+4 -1
View File
@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { computed } from 'vue' import { computed } from 'vue'
import { useSession } from '../../store/session' import { useSession } from '../../store/session'
const { t } = useI18n()
const toast = useToast() const toast = useToast()
const sessionStore = useSession() const sessionStore = useSession()
@@ -31,7 +34,7 @@ const startSession = async () => {
:icon="['fas', 'play']" :icon="['fas', 'play']"
class="text-text-primary h-full px-3" 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> </button>
</div> </div>
</template> </template>
+6 -3
View File
@@ -1,7 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { useSession, useTerminal } from '../../store/session' import { useSession, useTerminal } from '../../store/session'
const { t } = useI18n()
const sessionStore = useSession() const sessionStore = useSession()
const terminal = ref<HTMLElement | null>(null) const terminal = ref<HTMLElement | null>(null)
@@ -24,14 +27,14 @@ onMounted(() => {
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex items-center gap-2 p-1"> <div class="flex items-center gap-2 p-1">
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text" /> <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 <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}`" :href="`/terminal/${sessionStore.session?.containerName}?token=${sessionStore.session?.token}`"
target="_blank" target="_blank"
> >
Open Terminal in New Tab {{ t('game.open_terminal_in_new_tab') }}
</a> </a>
</div> </div>
<div <div
+15 -1
View File
@@ -77,7 +77,8 @@
}, },
"quest_map": { "quest_map": {
"title": "Quest Map", "title": "Quest Map",
"intro": "Embark on your Linux adventure!" "intro": "Embark on your Linux adventure!",
"load_quest_map_failed": "Failed to Load Quest Map"
}, },
"quest": { "quest": {
"start": "Start Adventure", "start": "Start Adventure",
@@ -89,5 +90,18 @@
"score": "Score", "score": "Score",
"completed_quests": "Completed Quests", "completed_quests": "Completed Quests",
"load_leaderboard_failed": "Failed to Load Leaderboard" "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"
} }
} }
+15 -1
View File
@@ -77,7 +77,8 @@
}, },
"quest_map": { "quest_map": {
"title": "關卡地圖", "title": "關卡地圖",
"intro": "踏上你的Linux冒險之旅吧!" "intro": "踏上你的Linux冒險之旅吧!",
"load_quest_map_failed": "無法讀取關卡地圖"
}, },
"quest": { "quest": {
"start": "開始冒險", "start": "開始冒險",
@@ -89,5 +90,18 @@
"score": "分數", "score": "分數",
"completed_quests": "完成關卡", "completed_quests": "完成關卡",
"load_leaderboard_failed": "無法讀取排行榜" "load_leaderboard_failed": "無法讀取排行榜"
},
"game": {
"start": "開始",
"task": "任務:",
"loading": "載入中...",
"description": "說明",
"hint": "提示",
"terminal": "終端機",
"open_terminal_in_new_tab": "在新分頁開啟終端機",
"reset": "重置",
"continue": "繼續",
"command_book": "指令之書",
"cheatsheets": "指令列表"
} }
} }