- 음악퀴즈 파일제거 도구 신규 추가: 동의 → 휴지통/완전삭제 선택 → 커스텀 폴더 (현재값 + 기본 .mc_custom) 전체, 데스크톱 바로가기, gameDir 가 해당 폴더인 마인크래프트 런처 프로필 정리. - MC_CUSTOM_DIR .env 로 커스텀 게임 폴더 이름 유동화(.mc_custom 기본). 렌더러 사전에도 실제 폴더명 반영. - 간편포트포워딩: 실행 중 UPnP 매핑 유지, 창 닫힘/종료 시 자동 제거(activePort 추적). - 개발자용 빌드는 창/헤더 제목 앞에 (개발자용) 표기, exe 이름에도 반영. - exe 이름 변경: 음악퀴즈 간편설치기 / 음악퀴즈 리소스팩설치기 / 간편포트포워딩. - README 및 .env 템플릿 갱신, 버전 0.3.23 → 0.4.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
/**
|
|
* 단순 키-문자열 사전. 중첩 객체도 허용해서 그룹화 가능.
|
|
* { step1: { title: '1단계. 음악퀴즈 선택' } }
|
|
* t('step1.title') → '1단계. 음악퀴즈 선택'
|
|
*/
|
|
export type Locale = Record<string, unknown>
|
|
|
|
/**
|
|
* 자유 형식 ko-kr.json 을 로드하고 `t(key, params)` 헬퍼를 만들어 반환.
|
|
*
|
|
* 사용 패턴:
|
|
* const { t, dict } = createI18n(path.join(__dirname, 'locales', 'ko-kr.json'))
|
|
* t('step1.title')
|
|
* t('install.downloading', { idx: 3 }) // → '3번 노래 다운로드 중…'
|
|
*
|
|
* 키가 사전에 없으면 키 자체를 반환(개발 중 누락 빨리 찾도록).
|
|
* 사전이 비어 있어도 빌드는 깨지지 않고 키만 노출.
|
|
*/
|
|
export interface I18n {
|
|
/** 키로 문자열 lookup. 누락 시 키 그대로 반환. */
|
|
t(key: string, params?: Record<string, string | number>): string
|
|
/** 렌더러로 전달하기 위한 원본 사전(JSON 그대로). */
|
|
dict: Locale
|
|
}
|
|
|
|
export function createI18n(filePath: string): I18n {
|
|
let dict: Locale = {}
|
|
try {
|
|
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
dict = JSON.parse(raw) as Locale
|
|
} catch {
|
|
// 파일이 없거나 깨진 경우 빈 사전. t() 가 키 자체를 돌려주므로 UI 가 깨지진 않음.
|
|
dict = {}
|
|
}
|
|
|
|
function lookup(key: string): string | undefined {
|
|
const parts = key.split('.')
|
|
let cur: unknown = dict
|
|
for (const p of parts) {
|
|
if (cur && typeof cur === 'object' && p in (cur as Record<string, unknown>)) {
|
|
cur = (cur as Record<string, unknown>)[p]
|
|
} else {
|
|
return undefined
|
|
}
|
|
}
|
|
return typeof cur === 'string' ? cur : undefined
|
|
}
|
|
|
|
function interpolate(tpl: string, params?: Record<string, string | number>): string {
|
|
if (!params) return tpl
|
|
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name: string) => {
|
|
return name in params ? String(params[name]) : `{{${name}}}`
|
|
})
|
|
}
|
|
|
|
return {
|
|
t(key, params) {
|
|
const found = lookup(key)
|
|
return interpolate(found ?? key, params)
|
|
},
|
|
dict
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 진입점에서 호출할 표준 로더. 컴포넌트 이름과 `__dirname`(컴파일 후) 만 주면
|
|
* `locales/<component>/ko-kr.json` 을 찾아 로드.
|
|
*
|
|
* 탐색 순서(처음 발견된 것만 사용):
|
|
* 1. 패키징된 Electron 앱이면 `process.resourcesPath/locales/<component>/ko-kr.json`
|
|
* 2. `<프로젝트 루트>/locales/<component>/ko-kr.json`
|
|
*/
|
|
export function loadComponentI18n(
|
|
component: 'server' | 'installer' | 'installer-rp' | 'installer-pf' | 'installer-uninstall'
|
|
): I18n {
|
|
// 컴파일된 dist/shared/i18n.js 기준으로 프로젝트 루트는 2단계 위.
|
|
const projectRoot = path.resolve(__dirname, '..', '..')
|
|
|
|
const candidates: string[] = []
|
|
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath
|
|
if (typeof resourcesPath === 'string' && resourcesPath.length > 0) {
|
|
candidates.push(path.join(resourcesPath, 'locales', component, 'ko-kr.json'))
|
|
}
|
|
candidates.push(path.join(projectRoot, 'locales', component, 'ko-kr.json'))
|
|
|
|
for (const p of candidates) {
|
|
if (fs.existsSync(p)) {
|
|
return createI18n(p)
|
|
}
|
|
}
|
|
return createI18n(candidates[candidates.length - 1] ?? '')
|
|
}
|