feat: configurable locales path per story, dynamic language switching from story data
This commit is contained in:
@@ -96,11 +96,17 @@ export interface EndingDef {
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
export interface LocalesConfig {
|
||||
path: string
|
||||
languages: string[]
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
scenes: Record<string, SceneNode>
|
||||
startScene: string
|
||||
variables: Record<string, number>
|
||||
assetBase?: string
|
||||
locales?: LocalesConfig
|
||||
chapters?: ChapterInfo[]
|
||||
achievements?: AchievementDef[]
|
||||
endings?: EndingDef[]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"assetBase": "demo/",
|
||||
"locales": {
|
||||
"path": "locales/",
|
||||
"languages": ["zh", "en", "ja"]
|
||||
},
|
||||
"startScene": "intro",
|
||||
"variables": {
|
||||
"trust": 50,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user