feat: configurable locales path per story, dynamic language switching from story data

This commit is contained in:
2026-06-10 12:17:52 +08:00
parent 4cf2263c78
commit 86a0aebdc8
10 changed files with 64 additions and 28 deletions

View File

@@ -17,7 +17,7 @@ import AccessibilitySettings from '@/components/AccessibilitySettings.vue'
import { useGameEngine } from '@/composables/useGameEngine'
import { useGameStore } from '@/stores/gameStore'
import { useFullscreen } from '@/composables/useFullscreen'
import { useI18n } from '@/composables/useI18n'
import { useI18n, initStoryLocales } from '@/composables/useI18n'
const store = useGameStore()
const { t, currentLang } = useI18n()
@@ -62,6 +62,10 @@ async function resolveScenePath(): Promise<string> {
async function init() {
const scenePath = await resolveScenePath()
await loadGame(scenePath)
const lc = store.storyLocales
if (lc.path) {
await initStoryLocales(lc.path)
}
loading.value = false
hasAutoSave.value = (await saveSystem.load(0)) !== null
}

View File

@@ -1,23 +1,30 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n'
import { useGameStore } from '@/stores/gameStore'
const { currentLang, setLang, t } = useI18n()
const { currentLang, setLang } = useI18n()
const store = useGameStore()
const langLabels: Record<string, string> = {
zh: '中文',
en: 'English',
ja: '日本語',
ko: '한국어',
fr: 'Français',
de: 'Deutsch',
es: 'Español',
pt: 'Português',
}
</script>
<template>
<div class="lang-switch">
<div class="lang-switch" v-if="store.storyLocales.languages.length > 1">
<button
:class="['lang-btn', { active: currentLang === 'zh' }]"
@click="setLang('zh')"
>中文</button>
<button
:class="['lang-btn', { active: currentLang === 'en' }]"
@click="setLang('en')"
>English</button>
<button
:class="['lang-btn', { active: currentLang === 'ja' }]"
@click="setLang('ja')"
>日本語</button>
v-for="lang in store.storyLocales.languages"
:key="lang"
:class="['lang-btn', { active: currentLang === lang }]"
@click="setLang(lang)"
>{{ langLabels[lang] || lang.toUpperCase() }}</button>
</div>
</template>

View File

@@ -161,6 +161,7 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
engine.achievementSystem.init(data.achievements || [], achieved)
store.setEndings(data.endings || [])
store.setStoryLocales(data.locales)
const visitedIds = await saveSystem.getVisitedSceneIds()
store.setVisitedSceneIds(visitedIds)

View File

@@ -4,20 +4,28 @@ import enUI from '@/locales/en.json'
import jaUI from '@/locales/ja.json'
const uiMessages = { zh: zhUI, en: enUI, ja: jaUI } as const
type Lang = 'zh' | 'en' | 'ja'
const currentLang = ref<Lang>(
(localStorage.getItem('lang') as Lang) || 'zh',
)
type Lang = string
const currentLang = ref<Lang>(localStorage.getItem('lang') || 'zh')
const storyMessages = ref<Record<string, Record<string, any>>>({})
const storyLoading = new Set<Lang>()
const storyLoading = new Set<string>()
let localesPath = ''
async function loadStoryMessages(lang: Lang) {
if (storyMessages.value[lang] || storyLoading.has(lang)) return
function resolveLocalePath(lang: string): string {
const base = localesPath.endsWith('/') ? localesPath : localesPath + '/'
return base + lang + '.json'
}
export async function initStoryLocales(path: string, lang?: string) {
localesPath = path
return loadStoryMessages(lang || currentLang.value)
}
async function loadStoryMessages(lang: string) {
if (!localesPath || storyMessages.value[lang] || storyLoading.has(lang)) return
storyLoading.add(lang)
try {
const resp = await fetch(`/locales/${lang}.json`)
const resp = await fetch(resolveLocalePath(lang))
if (resp.ok) {
storyMessages.value = {
...storyMessages.value,
@@ -31,8 +39,6 @@ async function loadStoryMessages(lang: Lang) {
}
}
loadStoryMessages(currentLang.value)
function t(key: string): string {
const parts = key.split('.')
@@ -42,7 +48,8 @@ function t(key: string): string {
}
if (typeof result === 'string') return result
let fallback: any = uiMessages[currentLang.value]
const uiLang = currentLang.value as keyof typeof uiMessages
let fallback: any = (uiMessages as any)[uiLang] || uiMessages.zh
for (const p of parts) {
fallback = fallback?.[p]
}
@@ -51,7 +58,7 @@ function t(key: string): string {
return key
}
async function setLang(lang: Lang) {
async function setLang(lang: string) {
await loadStoryMessages(lang)
currentLang.value = lang
localStorage.setItem('lang', lang)

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, shallowRef } from 'vue'
import type { SceneNode, Choice, QTEDefinition, Hotspot, ChapterInfo, AchievementDef, EndingDef } from '@engine/types'
import type { SceneNode, Choice, QTEDefinition, Hotspot, ChapterInfo, AchievementDef, EndingDef, LocalesConfig } from '@engine/types'
export interface SlotInfo {
slot: number
@@ -36,6 +36,7 @@ export const useGameStore = defineStore('game', () => {
const showEndingGallery = ref(false)
const endings = ref<EndingDef[]>([])
const visitedSceneIds = ref<Set<string>>(new Set())
const storyLocales = ref<LocalesConfig>({ path: '', languages: ['zh'] })
const subFontSize = ref(Number(localStorage.getItem('subFontSize') || 20))
const subBgAlpha = ref(Number(localStorage.getItem('subBgAlpha') || 0))
@@ -180,6 +181,10 @@ export const useGameStore = defineStore('game', () => {
visitedSceneIds.value = new Set(visitedSceneIds.value)
}
function setStoryLocales(locales: LocalesConfig | undefined) {
if (locales) storyLocales.value = locales
}
function setSubFontSize(v: number) { subFontSize.value = v; localStorage.setItem('subFontSize', String(v)) }
function setSubBgAlpha(v: number) { subBgAlpha.value = v; localStorage.setItem('subBgAlpha', String(v)) }
function setQteTimeRelax(v: boolean) { qteTimeRelax.value = v; localStorage.setItem('qteTimeRelax', String(v)) }
@@ -206,6 +211,7 @@ export const useGameStore = defineStore('game', () => {
hotspots, isImageScene, showChapterSelect, chapters, unlockedChapterIds,
inputMode, showAchievements, achievementDefs, unlockedAchievementIds,
toastAchievementId, showEndingGallery, endings, visitedSceneIds,
storyLocales,
subFontSize, subBgAlpha, qteTimeRelax, qteSingleKey, antiMistap, pauseEnabled,
showSettings,
setScene, setChoices, clearChoices, setGameEnded,
@@ -217,6 +223,7 @@ export const useGameStore = defineStore('game', () => {
setShowAchievements, setAchievementDefs, setUnlockedAchievementIds,
addUnlockedAchievement, clearToastAchievement,
setEndings, setShowEndingGallery, setVisitedSceneIds, addVisitedSceneId,
setStoryLocales,
setSubFontSize, setSubBgAlpha, setQteTimeRelax, setQteSingleKey, setAntiMistap, setPauseEnabled,
setShowSettings,
dump,