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:
2026-05-13 04:00:31 +09:00
parent 135bc98840
commit 6cd402121b
10 changed files with 314 additions and 101 deletions

View File

@@ -10,6 +10,25 @@ const state = {
resourcepackPath: ''
}
let I18N = {}
function tt(key, params) {
var parts = String(key).split('.')
var cur = I18N
for (var i = 0; i < parts.length; i++) {
if (cur && typeof cur === 'object' && parts[i] in cur) {
cur = cur[parts[i]]
} else {
return key
}
}
if (typeof cur !== 'string') return key
if (!params) return cur
return cur.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_m, name) {
return name in params ? String(params[name]) : '{{' + name + '}}'
})
}
const pageHost = document.getElementById('pageHost')
const stepIndicator = document.getElementById('stepIndicator')
const logViewer = document.getElementById('logViewer')
@@ -20,10 +39,10 @@ logToggle.addEventListener('click', function () {
logViewer.classList.toggle('collapsed')
if (logViewer.classList.contains('collapsed')) {
logViewer.style.height = '36px'
logToggle.textContent = '펼치기'
logToggle.textContent = tt('logViewer.expand')
} else {
logViewer.style.height = ''
logToggle.textContent = '접기'
logToggle.textContent = tt('logViewer.collapse')
}
})
@@ -33,6 +52,22 @@ api.onLog(function (line) {
logBody.scrollTop = logBody.scrollHeight
})
function applyStaticI18n() {
document.title = tt('app.title')
var h1 = document.querySelector('.appHeader h1')
if (h1) h1.textContent = tt('app.title')
var stepLis = stepIndicator.querySelectorAll('li')
stepLis.forEach(function (item) {
var idx = item.getAttribute('data-step')
if (idx === '1') item.textContent = tt('stepIndicator.step1')
else if (idx === '2') item.textContent = tt('stepIndicator.step2')
else if (idx === '3') item.textContent = tt('stepIndicator.step3')
})
var logH2 = logViewer.querySelector('header h2')
if (logH2) logH2.textContent = tt('logViewer.heading')
logToggle.textContent = tt('logViewer.collapse')
}
function setActiveStep(step) {
stepIndicator.querySelectorAll('li').forEach(function (item) {
var index = Number(item.getAttribute('data-step'))
@@ -51,9 +86,9 @@ function renderStep1() {
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>1단계. 음악퀴즈 선택</h2>' +
'<div id="packList" class="cardChoice"><p class="formMessage">목록을 불러오는 중...</p></div>' +
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</button></div>'
'<h2>' + escapeHtml(tt('step1.heading')) + '</h2>' +
'<div id="packList" class="cardChoice"><p class="formMessage">' + escapeHtml(tt('common.loading')) + '</p></div>' +
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>' + escapeHtml(tt('common.next')) + '</button></div>'
pageHost.appendChild(section)
var listEl = section.querySelector('#packList')
var nextBtn = section.querySelector('#next')
@@ -61,7 +96,7 @@ function renderStep1() {
function renderList() {
listEl.innerHTML = ''
if (state.packs.length === 0) {
listEl.innerHTML = '<p class="formMessage error">등록된 음악퀴즈가 없습니다.</p>'
listEl.innerHTML = '<p class="formMessage error">' + escapeHtml(tt('common.noPacks')) + '</p>'
return
}
state.packs.forEach(function (pack) {
@@ -69,11 +104,14 @@ function renderStep1() {
card.type = 'button'
card.className = 'choiceCard'
if (state.selectedKey === pack.key) card.classList.add('selected')
var verLabel = pack.mcVersion ? '마인크래프트 ' + escapeHtml(pack.mcVersion) + ' · ' : ''
var verLabel = pack.mcVersion
? escapeHtml(tt('common.mcVersionLabel', { version: pack.mcVersion }))
: ''
card.innerHTML =
'<strong>' + escapeHtml(pack.name) + '</strong>' +
'<small>' + verLabel +
'음악 ' + pack.list.music.length + '곡 · 사진 ' + pack.list.images.length + '장</small>'
escapeHtml(tt('common.trackImageCount', { music: pack.list.music.length, image: pack.list.images.length })) +
'</small>'
card.addEventListener('click', function () {
state.selectedKey = pack.key
nextBtn.disabled = false
@@ -88,7 +126,7 @@ function renderStep1() {
api.selectPack(state.selectedKey).then(function () {
renderStep2()
}).catch(function (err) {
alert(err.message || '선택 실패')
alert(err.message || tt('common.selectFailed'))
})
})
@@ -96,7 +134,9 @@ function renderStep1() {
state.packs = packs || []
renderList()
}).catch(function (err) {
listEl.innerHTML = '<p class="formMessage error">목록 로드 실패: ' + escapeHtml(err.message || '') + '</p>'
listEl.innerHTML = '<p class="formMessage error">' +
escapeHtml(tt('common.listLoadFailed', { message: err.message || '' })) +
'</p>'
})
}
@@ -115,30 +155,29 @@ function renderStep2() {
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>2단계. 리소스팩 설치</h2>' +
'<p class="formMessage">음악·사진을 받아 리소스팩을 만들고 ' +
'<code>%appdata%/.mc_custom/resourcepacks/</code> 에 자동 설치합니다.</p>' +
'<h2>' + escapeHtml(tt('step2.heading')) + '</h2>' +
'<p class="formMessage">' + tt('step2.description') + '</p>' +
'<div class="prepRow">' +
' <span class="prepChip" id="chip-ytdlp">yt-dlp 준비</span>' +
' <span class="prepChip" id="chip-ffmpeg">ffmpeg 준비</span>' +
' <span class="prepChip" id="chip-ytdlp">' + escapeHtml(tt('step2.chipYtdlp')) + '</span>' +
' <span class="prepChip" id="chip-ffmpeg">' + escapeHtml(tt('step2.chipFfmpeg')) + '</span>' +
'</div>' +
'<div class="progressSection">' +
' <h3>음악 다운로드</h3>' +
' <div class="sectionSub" id="music-sub">' + musicTotal + '</div>' +
' <h3>' + escapeHtml(tt('step2.musicHeading')) + '</h3>' +
' <div class="sectionSub" id="music-sub">' + escapeHtml(tt('step2.musicSub', { count: musicTotal })) + '</div>' +
' <div class="progressGrid" id="musicGrid"></div>' +
'</div>' +
'<div class="progressSection">' +
' <h3>사진 다운로드</h3>' +
' <div class="sectionSub" id="image-sub">' + imageTotal + '</div>' +
' <h3>' + escapeHtml(tt('step2.imageHeading')) + '</h3>' +
' <div class="sectionSub" id="image-sub">' + escapeHtml(tt('step2.imageSub', { count: imageTotal })) + '</div>' +
' <div class="progressGrid" id="imageGrid"></div>' +
'</div>' +
'<div class="progressSection">' +
' <h3>리소스팩 빌드</h3>' +
' <div class="sectionSub" id="pkg-sub">대기 중…</div>' +
' <h3>' + escapeHtml(tt('step2.packageHeading')) + '</h3>' +
' <div class="sectionSub" id="pkg-sub">' + escapeHtml(tt('step2.packageWaiting')) + '</div>' +
'</div>' +
'<div class="actionRow">' +
' <span></span>' +
' <button class="dangerBtn" id="cancel">취소</button>' +
' <button class="dangerBtn" id="cancel">' + escapeHtml(tt('common.cancel')) + '</button>' +
'</div>'
pageHost.appendChild(section)
@@ -156,7 +195,7 @@ function renderStep2() {
card.innerHTML =
'<div class="cardTop"><span class="label">' + idx + '</span><span class="icon">○</span></div>' +
'<div class="bar"><span></span></div>' +
'<div class="pct">대기</div>'
'<div class="pct">' + escapeHtml(tt('step2.cardWaiting')) + '</div>'
return card
}
for (var m = 1; m <= musicTotal; m++) musicGrid.appendChild(buildCard(m))
@@ -172,17 +211,17 @@ function renderStep2() {
var pct = card.querySelector('.pct')
var icon = card.querySelector('.icon')
if (status === 'done') {
if (pct) pct.textContent = '완료'
if (pct) pct.textContent = tt('step2.cardDone')
if (icon) icon.textContent = '✓'
if (bar) bar.style.width = '100%'
} else if (status === 'error') {
if (pct) pct.textContent = '실패'
if (pct) pct.textContent = tt('step2.cardError')
if (icon) icon.textContent = '✕'
} else if (status === 'running') {
if (pct) pct.textContent = Math.round(percent) + '%'
if (icon) icon.textContent = '⏳'
} else {
if (pct) pct.textContent = '대기'
if (pct) pct.textContent = tt('step2.cardWaiting')
if (icon) icon.textContent = '○'
}
}
@@ -209,7 +248,9 @@ function renderStep2() {
return
}
if (payload.phase === 'package') {
pkgSub.textContent = payload.done ? '설치 완료' : (payload.message || '빌드 중…')
pkgSub.textContent = payload.done
? tt('step2.packageDone')
: (payload.message || tt('step2.packageBuilding'))
return
}
})
@@ -232,7 +273,7 @@ function renderStep2() {
}).catch(function (err) {
state.installing = false
if (stopProgress) stopProgress()
alert('설치 실패: ' + ((err && err.message) || err))
alert(tt('common.installFailed', { message: (err && err.message) || err }))
renderStep1()
})
}
@@ -244,14 +285,14 @@ function renderStep3() {
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>3단계. 완료</h2>' +
'<p class="formMessage">리소스팩 설치를 완료했습니다.</p>' +
'<h2>' + escapeHtml(tt('step3.heading')) + '</h2>' +
'<p class="formMessage">' + escapeHtml(tt('step3.message')) + '</p>' +
(state.resourcepackPath
? '<p class="formMessage"><code>' + escapeHtml(state.resourcepackPath) + '</code></p>'
: '') +
'<div class="actionRow">' +
' <button class="secondaryBtn" id="openFolder">리소스팩 폴더 열기</button>' +
' <button class="primaryBtn" id="finish">확인</button>' +
' <button class="secondaryBtn" id="openFolder">' + escapeHtml(tt('common.openFolder')) + '</button>' +
' <button class="primaryBtn" id="finish">' + escapeHtml(tt('common.confirm')) + '</button>' +
'</div>'
pageHost.appendChild(section)
section.querySelector('#openFolder').addEventListener('click', function () {
@@ -268,4 +309,8 @@ function escapeHtml(s) {
})
}
renderStep1()
;(async function () {
try { I18N = (await api.loadLocale()) || {} } catch (_) { I18N = {} }
applyStaticI18n()
renderStep1()
})()