Feature/multilingual quests (#209)

This commit is contained in:
Justin Lin authored and GitHub committed 2026-10-09 22:58:05 +11:00
commit 4260e742df
154 files changed
+2712 -108

No files matched your search

+11 -4
View File
@@ -6,7 +6,7 @@ import { ref, defineProps, onMounted } from 'vue'
import MarkdownText from './MarkdownText.vue'
import { trpc } from '../utils/trpc'
const { t } = useI18n()
const { t, locale } = useI18n()
const props = defineProps({
questTitle: {
@@ -52,8 +52,15 @@ const handleColour = () => {
}
}
onMounted(async () => {
const quest = await trpc.quests.getQuestDetail.query(questId)
questInstruction.value = quest.instruction
try {
const quest = await trpc.quests.getQuestDetail.query({
questId,
locale: locale.value,
})
questInstruction.value = quest.instruction
} catch (error) {
toast.error('Failed to load quest detail:', error)
}
handleColour()
})
</script>
@@ -83,7 +90,7 @@ onMounted(async () => {
target="_blank"
@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"
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full"
>
{{ t('quest.start') }}
</a>
+7 -2
View File
@@ -7,7 +7,7 @@ import { trpc } from '../utils/trpc'
import useUserProfile from '../store/userProfile'
import QuestIntro from './QuestIntro.vue'
const { t } = useI18n()
const { t, locale } = useI18n()
const store = useUserProfile()
const toast = useToast()
@@ -66,7 +66,8 @@ async function computeGraphData() {
if (!store.progress) return
try {
const quests = await trpc.quests.getQuests.query()
const quests = await trpc.quests.getQuests.query({ locale: locale.value })
const dag = new DAG(quests)
const nodesValues = dag.getNodes()
@@ -117,6 +118,10 @@ onMounted(async () => {
})
watch(() => store.progress, computeGraphData, { deep: true })
watch(locale, () => {
opened.value = null
computeGraphData()
})
const curvedPath = computed(() => {
return (source: Node, target: Node) => {
+31 -2
View File
@@ -4,15 +4,44 @@ import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { useSession } from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
import { trpc } from '../../utils/trpc'
import type { QuestLocale } from '../../../../packages/constants'
const { t } = useI18n()
const { t, locale } = useI18n()
const toast = useToast()
const router = useRouter()
const sessionStore = useSession()
const reset = async () => {
console.log('resetting...')
await sessionStore.createSession()
const selectedLocale = locale.value as QuestLocale
const questId = sessionStore.session?.quest
if (!questId) return
try {
const quests = await trpc.quests.getQuests.query({
locale: selectedLocale,
})
console.log({
selectedLocale,
questId,
availableQuestIds: quests.map((quest) => quest.id),
})
if (!quests.some((quest) => quest.id === questId)) {
toast.warning(t('game.language_unavailable'))
return
}
await sessionStore.createSession()
} catch (error) {
console.error('Failed to reset quest:', error)
toast.error(t('game.reset_failed'))
}
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
+12 -5
View File
@@ -1,12 +1,17 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { setLocale, type SupportedLocale } from '../../i18n'
import { setLocale } from '../../i18n'
import type { QuestLocale } from '../../../../packages/constants'
import {
SUPPORTED_LOCALES,
LOCALE_NAME_MAP,
} from '../../../../packages/constants'
const { locale } = useI18n({ useScope: 'global' })
const { t, locale } = useI18n({ useScope: 'global' })
function changeLanguage(event: Event) {
const selectedLocale = (event.target as HTMLSelectElement)
.value as SupportedLocale
.value as QuestLocale
setLocale(selectedLocale)
}
@@ -15,12 +20,14 @@ function changeLanguage(event: Event) {
<template>
<select
:value="locale"
:title="t('language.change')"
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>
<option v-for="locale in SUPPORTED_LOCALES" :key="locale" :value="locale">
{{ LOCALE_NAME_MAP[locale] }}
</option>
</select>
</template>
+6 -8
View File
@@ -1,16 +1,14 @@
import { createI18n } from 'vue-i18n'
import type { QuestLocale } from '../../../packages/constants'
import { SUPPORTED_LOCALES } from '../../../packages/constants'
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 isSupportedLocale(locale: string): locale is QuestLocale {
return SUPPORTED_LOCALES.includes(locale as QuestLocale)
}
function getInitialLocale(): SupportedLocale {
function getInitialLocale(): QuestLocale {
const savedLocale = localStorage.getItem('locale')
if (savedLocale && isSupportedLocale(savedLocale)) {
@@ -34,7 +32,7 @@ export const i18n = createI18n({
},
})
export function setLocale(locale: SupportedLocale) {
export function setLocale(locale: QuestLocale) {
i18n.global.locale.value = locale
localStorage.setItem('locale', locale)
document.documentElement.lang = locale
+5 -4
View File
@@ -1,8 +1,6 @@
{
"language": {
"change": "Change language",
"traditional_chinese": "繁體中文",
"english": "English"
"change": "Change language"
},
"landing_nav": {
"intro": "Introduction",
@@ -102,6 +100,9 @@
"reset": "Reset",
"continue": "Continue",
"command_book": "Command Book",
"cheatsheets": "Cheat Sheets"
"cheatsheets": "Cheat Sheets",
"reset_to_apply_language": "Reset quest to apply language changes",
"reset_failed": "Reset failed",
"language_unavailable": "This quest does not support the selected language, please choose another language"
}
}
+5 -4
View File
@@ -1,8 +1,6 @@
{
"language": {
"change": "切換語言",
"traditional_chinese": "繁體中文",
"english": "English"
"change": "切換語言"
},
"landing_nav": {
"intro": "前言",
@@ -102,6 +100,9 @@
"reset": "重置",
"continue": "繼續",
"command_book": "指令之書",
"cheatsheets": "指令列表"
"cheatsheets": "指令列表",
"reset_to_apply_language": "重置關卡以套用語言變更",
"reset_failed": "重置失敗",
"language_unavailable": "此關卡不支援所選語言,請選擇其他語言"
}
}
+14 -7
View File
@@ -1,4 +1,5 @@
import { defineStore } from 'pinia'
import type { QuestLocale } from '../../../packages/constants'
import {
FileGraph,
type FileGraphUpdateEvent,
@@ -9,6 +10,7 @@ import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import type { Session } from '../types'
import { trpc } from '../utils/trpc'
import { i18n } from '../i18n'
const socket = new Socket()
const term = new SocketTerminal()
@@ -41,11 +43,16 @@ export const useSession = defineStore('session', {
quest: null,
}),
actions: {
async setQuest(questId: string) {
this.quest = await trpc.quests.getQuestDetail.query(questId)
async setQuest(questId: string, locale: QuestLocale) {
this.quest = await trpc.quests.getQuestDetail.query({
questId,
locale,
})
this.questId = questId
},
async setSession(session: SessionDetail) {
await this.setQuest(session.quest, session.locale)
this.session = {
...session,
graph: new FileGraph(session.graph),
@@ -60,18 +67,18 @@ export const useSession = defineStore('session', {
term.focus()
},
async createSession() {
const locale = i18n.global.locale.value
const session = await trpc.session.createSession.mutate({
questId: this.questId,
locale,
})
await this.setSession(session)
term.send('echo start\n')
},
async getActiveSession() {
const session = await trpc.session.getActiveSession.query({
questId: this.questId,
})
async getActiveSession(questId: string) {
const session = await trpc.session.getActiveSession.query({ questId })
if (session) {
this.setSession(session)
await this.setSession(session)
}
},
newUpdate(update: SessionUpdate) {
+17 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
import { computed, onMounted, watch } from 'vue'
import { useSession } from '../store/session'
import useUserProfile from '../store/userProfile'
@@ -13,6 +15,8 @@ import CompleteModal from '../components/game/CompleteModal.vue'
import StartButton from '../components/game/StartButton.vue'
import { openQuestSurvey } from '../utils/formbricks'
const { t, locale } = useI18n()
const toast = useToast()
const sessionStore = useSession()
const userStore = useUserProfile()
@@ -27,6 +31,13 @@ watch(completed, (newValue, oldValue) => {
}
})
watch(locale, (newLocale) => {
const { session } = sessionStore
if (session && newLocale !== session.locale) {
toast.info(t('game.reset_to_apply_language'))
}
})
const props = defineProps({
questId: {
type: String,
@@ -37,8 +48,12 @@ const props = defineProps({
onMounted(async () => {
sessionStore.reset()
sessionStore.setup()
await sessionStore.setQuest(props.questId)
await sessionStore.getActiveSession()
await sessionStore.getActiveSession(props.questId)
if (!sessionStore.session) {
await sessionStore.setQuest(props.questId, locale.value)
}
})
</script>