i18n: 리소스팩 설치기 UI 문구를 locales/installer-rp/ko-kr.json 으로 분리
- main/preload/ytdlp/ffmpeg/music/images/pack/renderer 전반에서 로그·에러·진행 메시지 문자열을 locales/installer-rp/ko-kr.json 사전 키로 교체 - preload 에 loadLocale 추가, main 에 rp:i18n:dict IPC 핸들러 추가 - 패키징된 .exe 에서도 한국어 사전이 적용되도록 electron-builder.yml 의 extraResources 에 locales/ 폴더 추가 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import type { Manifest, PackDefinition, PackList } from '../shared/types.js'
|
||||
import { normalizePackDefinition } from '../shared/store.js'
|
||||
import { getAppDataDir, getMcCustomDir } from '../shared/paths.js'
|
||||
import { loadEnv, getManifestUrl } from '../shared/env.js'
|
||||
import { loadComponentI18n } from '../shared/i18n.js'
|
||||
import type { RpFetchedPack } from './types.js'
|
||||
import { ensureYtDlpExe } from './ytdlp.js'
|
||||
import { ensureFfmpegExe } from './ffmpeg.js'
|
||||
@@ -19,6 +20,9 @@ import { downloadImage, normalizeToCover, coverFileName } from './images.js'
|
||||
import { buildResourcepackZip } from './pack.js'
|
||||
|
||||
loadEnv()
|
||||
const i18n = loadComponentI18n('installer-rp')
|
||||
const t = i18n.t
|
||||
export const localeDict = i18n.dict
|
||||
|
||||
interface RpInstallerState {
|
||||
manifestUrl: string
|
||||
@@ -154,7 +158,7 @@ function fetchBuffer(url: string): Promise<Buffer> {
|
||||
response.on('end', () => resolve(Buffer.concat(chunks)))
|
||||
})
|
||||
request.on('error', reject)
|
||||
request.on('timeout', () => request.destroy(new Error('요청 시간 초과')))
|
||||
request.on('timeout', () => request.destroy(new Error(t('common.requestTimeout'))))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,7 +173,7 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi
|
||||
state.manifestUrl = manifestUrlInput
|
||||
state.baseUrl = deriveBaseUrl(manifestUrlInput)
|
||||
}
|
||||
sendLog(`manifest 다운로드: ${state.manifestUrl}`)
|
||||
sendLog(t('log.manifestDownload', { url: state.manifestUrl }))
|
||||
const manifest = await fetchJson<Manifest>(state.manifestUrl)
|
||||
const results: RpFetchedPack[] = []
|
||||
for (const entry of manifest.packs ?? []) {
|
||||
@@ -181,7 +185,7 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi
|
||||
const [listRaw, packRaw] = await Promise.all([
|
||||
fetchJson<Partial<PackList>>(listUrl),
|
||||
fetchJson<Partial<PackDefinition>>(packUrl).catch((err) => {
|
||||
sendLog(`팩 정의 로드 실패 (${entry.file}): ${(err as Error).message} — mcVersion 폴백`)
|
||||
sendLog(t('log.packDefFailed', { file: entry.file, message: (err as Error).message }))
|
||||
return null
|
||||
})
|
||||
])
|
||||
@@ -202,31 +206,37 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi
|
||||
list
|
||||
})
|
||||
} catch (error) {
|
||||
sendLog(`목록 로드 실패 (${entry.file}): ${(error as Error).message}`)
|
||||
sendLog(t('log.listLoadFailed', { file: entry.file, message: (error as Error).message }))
|
||||
}
|
||||
}
|
||||
state.packs.clear()
|
||||
for (const item of results) state.packs.set(item.key, item)
|
||||
sendLog(`로드된 음악퀴즈: ${results.length}개`)
|
||||
sendLog(t('log.packsLoaded', { count: results.length }))
|
||||
for (const item of results) {
|
||||
sendLog(` - ${item.key}: mc=${item.mcVersion || '?'} 베이스=${item.resourcepackPath || '(없음)'}`)
|
||||
sendLog(t('log.packEntry', {
|
||||
key: item.key,
|
||||
mc: item.mcVersion || t('log.packEntryUnknownVersion'),
|
||||
base: item.resourcepackPath || t('log.packEntryNoBase')
|
||||
}))
|
||||
}
|
||||
return results
|
||||
})
|
||||
|
||||
ipcMain.handle('rp:packs:select', async (_event, packKey: string) => {
|
||||
if (!state.packs.has(packKey)) {
|
||||
throw new Error('선택한 음악퀴즈를 찾을 수 없습니다.')
|
||||
throw new Error(t('errors.selectedPackNotFound'))
|
||||
}
|
||||
state.selectedKey = packKey
|
||||
sendLog(`선택: ${packKey}`)
|
||||
sendLog(t('log.selectedPack', { key: packKey }))
|
||||
})
|
||||
|
||||
ipcMain.handle('rp:i18n:dict', () => localeDict)
|
||||
|
||||
// ── IPC: 2단계 설치 ──────────────────────────────────
|
||||
ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string }> => {
|
||||
if (!state.selectedKey) throw new Error('음악퀴즈를 먼저 선택해주세요.')
|
||||
if (!state.selectedKey) throw new Error(t('errors.selectPackFirst'))
|
||||
const pack = state.packs.get(state.selectedKey)
|
||||
if (!pack) throw new Error('선택된 음악퀴즈를 찾을 수 없습니다.')
|
||||
if (!pack) throw new Error(t('errors.currentPackNotFound'))
|
||||
state.cancelRequested = false
|
||||
|
||||
const tempRoot = path.join(getMcCustomDir(), '.temp')
|
||||
@@ -237,16 +247,16 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
|
||||
try {
|
||||
// 2-1. yt-dlp / ffmpeg 준비 (%appdata%/.mc_custom/{yt-dlp,ffmpeg}.exe)
|
||||
sendLog('yt-dlp 준비 중…')
|
||||
sendProgress({ phase: 'prep', message: 'yt-dlp 준비 중' })
|
||||
sendLog(t('log.ytdlpPreparing'))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ytdlpPreparing') })
|
||||
const ytDlpBin = await ensureYtDlpExe(sendLog)
|
||||
sendLog(`yt-dlp 경로: ${ytDlpBin}`)
|
||||
sendLog(t('log.ytdlpPath', { path: ytDlpBin }))
|
||||
throwIfCancelled()
|
||||
sendLog('ffmpeg 준비 중…')
|
||||
sendProgress({ phase: 'prep', message: 'ffmpeg 준비 중' })
|
||||
sendLog(t('log.ffmpegPreparing'))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ffmpegPreparing') })
|
||||
const ffmpegBin = await ensureFfmpegExe(sendLog)
|
||||
sendLog(`ffmpeg 경로: ${ffmpegBin}`)
|
||||
sendProgress({ phase: 'prep', message: '준비 완료', done: true })
|
||||
sendLog(t('log.ffmpegPath', { path: ffmpegBin }))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ready'), done: true })
|
||||
throwIfCancelled()
|
||||
|
||||
// 2-2. 음악 다운로드 (CPU 코어 수 기반 자동 동시 다운로드, 시차 출발, ogg 변환)
|
||||
@@ -256,8 +266,8 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
const cpuCount = os.cpus()?.length ?? 0
|
||||
// 첫 음악은 즉시 시작 가능하도록 base 를 현재 시각으로.
|
||||
nextMusicStartAt = Date.now()
|
||||
sendLog(`CPU 코어 ${cpuCount}개 감지 → 동시 다운로드 ${concurrency}개`)
|
||||
sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${concurrency}개, 시차 ${MUSIC_START_STAGGER_MS}ms)`)
|
||||
sendLog(t('log.cpuDetected', { cores: cpuCount, concurrency }))
|
||||
sendLog(t('log.musicStart', { total: musicTotal, concurrency, stagger: MUSIC_START_STAGGER_MS }))
|
||||
|
||||
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
|
||||
const musicList = pack.list.music
|
||||
@@ -272,7 +282,7 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
if (state.cancelRequested) return
|
||||
const entry = musicList[i]
|
||||
const idx = i + 1
|
||||
sendLog(`${idx}번 노래 다운로드 시작`)
|
||||
sendLog(t('log.musicTrackStart', { idx }))
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'running' })
|
||||
let child: ChildProcess | null = null
|
||||
try {
|
||||
@@ -296,16 +306,16 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
}
|
||||
})
|
||||
if (child) state.activeChildren.delete(child)
|
||||
sendLog(`${idx}번 노래 완료: ${path.basename(outPath)}`)
|
||||
sendLog(t('log.musicTrackDone', { idx, name: path.basename(outPath) }))
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 100, status: 'done' })
|
||||
} catch (err) {
|
||||
if (child) state.activeChildren.delete(child)
|
||||
if (state.cancelRequested) {
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: '취소됨' })
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: t('progress.cancelled') })
|
||||
return
|
||||
}
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||
throw new Error(`${idx}번 노래 다운로드 실패: ${(err as Error).message}`)
|
||||
throw new Error(t('errors.musicDownloadFailed', { idx, message: (err as Error).message }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,19 +329,19 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
// 2-3. 사진 다운로드 + painting variant 정규화
|
||||
const paintingDir = path.join(tempRoot, 'painting')
|
||||
await fsp.mkdir(paintingDir, { recursive: true })
|
||||
sendLog(`사진 다운로드 시작 (${imageTotal}장)`)
|
||||
sendLog(t('log.imageStart', { total: imageTotal }))
|
||||
for (let i = 0; i < imageTotal; i++) {
|
||||
throwIfCancelled()
|
||||
const entry = pack.list.images[i]
|
||||
const idx = i + 1
|
||||
sendLog(`${idx}번 사진 다운로드 중…`)
|
||||
sendLog(t('log.imageDownloading', { idx }))
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 10, status: 'running' })
|
||||
let buf: Buffer
|
||||
try {
|
||||
buf = await downloadImage(entry.url)
|
||||
} catch (err) {
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||
throw new Error(`${idx}번 사진 다운로드 실패: ${(err as Error).message}`)
|
||||
throw new Error(t('errors.imageDownloadFailed', { idx, message: (err as Error).message }))
|
||||
}
|
||||
throwIfCancelled()
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 60, status: 'running' })
|
||||
@@ -340,9 +350,9 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
await normalizeToCover(buf, outPath)
|
||||
} catch (err) {
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||
throw new Error(`${idx}번 사진 정규화 실패: ${(err as Error).message}`)
|
||||
throw new Error(t('errors.imageNormalizeFailed', { idx, message: (err as Error).message }))
|
||||
}
|
||||
sendLog(`${idx}번 사진 완료: ${path.basename(outPath)}`)
|
||||
sendLog(t('log.imageDone', { idx, name: path.basename(outPath) }))
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 100, status: 'done' })
|
||||
}
|
||||
|
||||
@@ -354,18 +364,18 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
const cleaned = pack.resourcepackPath.replace(/^\/+/, '')
|
||||
const baseUrl = `${state.baseUrl}/file/resourcepacks/${encodeURIComponent(cleaned)}`
|
||||
baseZipPath = path.join(tempRoot, 'base.zip')
|
||||
sendLog(`베이스 리소스팩 다운로드: ${cleaned}`)
|
||||
sendLog(` URL: ${baseUrl}`)
|
||||
sendProgress({ phase: 'package', message: '베이스 리소스팩 다운로드 중' })
|
||||
sendLog(t('log.baseDownload', { path: cleaned }))
|
||||
sendLog(t('log.baseUrl', { url: baseUrl }))
|
||||
sendProgress({ phase: 'package', message: t('progress.baseDownloading') })
|
||||
try {
|
||||
const buf = await fetchBuffer(baseUrl)
|
||||
await fsp.writeFile(baseZipPath, buf)
|
||||
sendLog(`베이스 리소스팩 받음 (${(buf.length / 1024).toFixed(1)} KB)`)
|
||||
sendLog(t('log.baseReceived', { kb: (buf.length / 1024).toFixed(1) }))
|
||||
} catch (err) {
|
||||
throw new Error(`베이스 리소스팩 다운로드 실패: ${(err as Error).message}`)
|
||||
throw new Error(t('errors.baseDownloadFailed', { message: (err as Error).message }))
|
||||
}
|
||||
} else {
|
||||
sendLog('베이스 리소스팩 없음(resourcepackPath 빈 값) — 새 리소스팩으로 생성')
|
||||
sendLog(t('log.baseAbsent'))
|
||||
}
|
||||
|
||||
// 2-5. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지, 베이스 위에 얹기)
|
||||
@@ -373,8 +383,8 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
const resourcepackName = `${state.selectedKey}_musicquiz.zip`
|
||||
const resourcepackDir = path.join(getMcCustomDir(), 'resourcepacks')
|
||||
const resourcepackPath = path.join(resourcepackDir, resourcepackName)
|
||||
sendLog(`리소스팩 zip 빌드 중… (${resourcepackName})`)
|
||||
sendProgress({ phase: 'package', message: baseZipPath ? '베이스에 음악·사진 추가 중' : 'zip 빌드 중' })
|
||||
sendLog(t('log.buildingZip', { name: resourcepackName }))
|
||||
sendProgress({ phase: 'package', message: baseZipPath ? t('progress.buildingWithBase') : t('progress.buildingZip') })
|
||||
await buildResourcepackZip({
|
||||
musicDir,
|
||||
paintingDir,
|
||||
@@ -387,8 +397,8 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
})
|
||||
|
||||
// 2-6. %appdata%/.mc_custom/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장)
|
||||
sendLog(`설치 완료: ${resourcepackPath}`)
|
||||
sendProgress({ phase: 'package', message: '설치 완료', done: true })
|
||||
sendLog(t('log.installComplete', { path: resourcepackPath }))
|
||||
sendProgress({ phase: 'package', message: t('progress.installComplete'), done: true })
|
||||
return { resourcepackPath }
|
||||
} finally {
|
||||
// 임시 파일 정리
|
||||
@@ -398,7 +408,7 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
|
||||
ipcMain.handle('rp:install:cancel', async () => {
|
||||
state.cancelRequested = true
|
||||
sendLog(`취소 요청됨. 실행 중 프로세스 ${state.activeChildren.size}개 중단…`)
|
||||
sendLog(t('log.cancelRequested', { count: state.activeChildren.size }))
|
||||
for (const child of state.activeChildren) {
|
||||
if (!child.killed) child.kill()
|
||||
}
|
||||
@@ -406,7 +416,7 @@ ipcMain.handle('rp:install:cancel', async () => {
|
||||
|
||||
function throwIfCancelled(): void {
|
||||
if (state.cancelRequested) {
|
||||
throw new Error('사용자가 설치를 취소했습니다.')
|
||||
throw new Error(t('errors.cancelledByUser'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user