import fs from 'node:fs' import path from 'node:path' /** * 단순 키-문자열 사전. 중첩 객체도 허용해서 그룹화 가능. * { step1: { title: '1단계. 음악퀴즈 선택' } } * t('step1.title') → '1단계. 음악퀴즈 선택' */ export type Locale = Record /** * 자유 형식 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 /** 렌더러로 전달하기 위한 원본 사전(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)) { cur = (cur as Record)[p] } else { return undefined } } return typeof cur === 'string' ? cur : undefined } function interpolate(tpl: string, params?: Record): 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//ko-kr.json` 을 찾아 로드. * * 탐색 순서(처음 발견된 것만 사용): * 1. 패키징된 Electron 앱이면 `process.resourcesPath/locales//ko-kr.json` * 2. `<프로젝트 루트>/locales//ko-kr.json` */ export function loadComponentI18n(component: 'server' | 'installer' | 'installer-rp'): 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] ?? '') }