// 다국어(i18n) 문자열 테이블. ko/en 두 언어를 지원한다. // 메인(메뉴/가이드)과 렌더러(UI)가 공용으로 사용한다. export type Lang = 'ko' | 'en' export const LANGS: Lang[] = ['ko', 'en'] export const DEFAULT_LANG: Lang = 'ko' export const LANG_LABEL: Record = { ko: '한국어', en: 'English' } /** 키 → 언어별 문자열. {token} 형태의 치환자를 지원. */ type Table = Record> export const MESSAGES: Table = { // 공통 'app.title': { ko: 'AI Photo Organizer', en: 'AI Photo Organizer' }, 'app.subtitle': { ko: '얼굴 인식 + 촬영일 기준 자동 사진 정리 · 로컬 전용', en: 'Auto-organize photos by face recognition + capture date · Local only' }, 'common.add': { ko: '추가', en: 'Add' }, // 온보딩 'onboard.title': { ko: '시작하기 전에', en: 'Before you start' }, 'onboard.subtitle': { ko: '언어와 테마를 선택하세요. 나중에 메뉴에서 바꿀 수 있습니다.', en: 'Choose your language and theme. You can change these later from the menu.' }, 'onboard.language': { ko: '언어', en: 'Language' }, 'onboard.theme': { ko: '테마', en: 'Theme' }, 'onboard.dark': { ko: '다크', en: 'Dark' }, 'onboard.light': { ko: '라이트', en: 'Light' }, 'onboard.start': { ko: '시작하기', en: 'Get Started' }, // 1. 프로필 'profile.section': { ko: '1. 인물 프로필', en: '1. Person Profiles' }, 'profile.count': { ko: '{n}/{max}명 · 순서 = 이동 우선순위', en: '{n}/{max} people · order = move priority' }, 'profile.namePlaceholder': { ko: '인물 이름 (예: Alex)', en: 'Person name (e.g. Alex)' }, 'profile.dndHint': { ko: '타일 클릭 · 드래그&드롭 · 붙여넣기(Ctrl+V)로 추가', en: 'Add by click, drag & drop, or paste (Ctrl+V)' }, 'profile.empty': { ko: '등록된 프로필이 없습니다. 이름을 추가하고 참조 얼굴 사진을 등록하세요.', en: 'No profiles yet. Add a name and register reference face photos.' }, 'profile.refCount': { ko: '참조 {n}장', en: '{n} refs' }, 'profile.delete': { ko: '프로필 삭제', en: 'Delete profile' }, 'profile.savePreset': { ko: '프리셋 저장', en: 'Save preset' }, 'profile.presets': { ko: '프리셋', en: 'Presets' }, 'profile.presetsHint': { ko: '자주 쓰는 인물을 저장해두고 클릭으로 불러옵니다 (로컬 전용)', en: 'Save people you use often and load them with a click (local only)' }, 'profile.presetsEmpty': { ko: "저장된 프리셋이 없습니다. 프로필의 '프리셋 저장'으로 추가하세요.", en: "No presets yet. Use 'Save preset' on a profile." }, 'profile.applyPresetTitle': { ko: '클릭해서 인물 프로필에 추가', en: 'Click to add to profiles' }, 'profile.deletePreset': { ko: '프리셋 삭제', en: 'Delete preset' }, 'profile.presetsFull': { ko: '프리셋이 가득 찼습니다 (최대 {max}개).', en: 'Presets are full (max {max}).' }, 'profile.profilesFull': { ko: '프로필이 가득 찼습니다 (최대 {max}명).', en: 'Profiles are full (max {max}).' }, 'profile.addFace': { ko: '얼굴 추가', en: 'Add face' }, 'profile.analyzing': { ko: '분석 중', en: 'Analyzing' }, 'profile.deletePhoto': { ko: '이 사진 삭제', en: 'Delete this photo' }, // 2. 폴더 'folder.section': { ko: '2. 폴더 선택', en: '2. Select Folders' }, 'folder.source': { ko: '정리할 폴더 (소스)', en: 'Folder to organize (source)' }, 'folder.output': { ko: '결과 저장 폴더 (출력)', en: 'Output folder' }, 'folder.unselected': { ko: '미선택', en: 'Not selected' }, 'folder.browse': { ko: '찾기', en: 'Browse' }, // 3. 실행 옵션 'run.section': { ko: '3. 실행 옵션', en: '3. Run Options' }, 'run.threshold': { ko: '매칭 임계값 ({v})', en: 'Match threshold ({v})' }, 'run.thresholdHint': { ko: '낮을수록 엄격', en: 'Lower = stricter' }, 'run.thresholdTip': { ko: '두 얼굴이 같은 사람인지 판단하는 거리 기준입니다.\n• 낮추면(예: 0.4) 더 엄격 → 다른 사람이 섞일 확률↓, 대신 본인을 놓칠 수 있음\n• 높이면(예: 0.6) 더 너그러움 → 본인을 잘 찾음, 대신 다른 사람이 섞일 수 있음\n권장: 0.45~0.55', en: 'Distance threshold for deciding whether two faces are the same person.\n• Lower (e.g. 0.4) = stricter → fewer false matches, but may miss the person\n• Higher (e.g. 0.6) = looser → catches the person more, but may include others\nRecommended: 0.45–0.55' }, 'run.concurrency': { ko: '동시 처리 ({v})', en: 'Concurrency ({v})' }, 'run.concurrencyTip': { ko: '한 번에 동시에 처리하는 파일 수입니다.\n• 높이면 처리 속도↑, 대신 메모리(RAM)·CPU 사용량↑\n• 사양이 낮거나 고해상도 사진이 많으면 2~3 권장', en: 'How many files are processed at the same time.\n• Higher = faster, but uses more memory (RAM) and CPU\n• On lower-end machines or with many high-resolution photos, 2–3 is recommended' }, 'run.detector': { ko: '검출 엔진', en: 'Detection engine' }, 'run.detectorSsd': { ko: '정확도 우선 (SSD MobileNet)', en: 'Accuracy first (SSD MobileNet)' }, 'run.detectorTiny': { ko: '속도 우선 (Tiny Face)', en: 'Speed first (Tiny Face)' }, 'run.noFaceWarn': { ko: "⚠️ 등록된 얼굴이 없습니다. 매칭 인물 없이 모두 'Unsorted' 폴더로 분류됩니다.", en: "⚠️ No registered faces. Everything will be sorted into the 'Unsorted' folder." }, 'run.start': { ko: '정리 시작', en: 'Start' }, 'run.rerun': { ko: '다시 실행', en: 'Run again' }, 'run.cancel': { ko: '취소', en: 'Cancel' }, 'run.reset': { ko: '초기화', en: 'Reset' }, // 진행/결과 'progress.section': { ko: '진행 상황', en: 'Progress' }, 'progress.waiting': { ko: '대기 중', en: 'Waiting' }, 'progress.scanning': { ko: '스캔 중…', en: 'Scanning…' }, 'progress.idle': { ko: '실행 대기', en: 'Idle' }, 'history.section': { ko: '처리 내역', en: 'History' }, 'history.recent': { ko: '최근 {n}건', en: 'Recent {n}' }, 'history.empty': { ko: '아직 처리된 파일이 없습니다.', en: 'No files processed yet.' }, 'kind.moved': { ko: '이동', en: 'Moved' }, 'kind.copied': { ko: '복사', en: 'Copied' }, 'kind.unmatched': { ko: '미정', en: 'Unsorted' }, 'kind.movie': { ko: '영상', en: 'Video' }, 'kind.failed': { ko: '실패', en: 'Failed' }, 'report.done': { ko: '✅ 작업 완료', en: '✅ Done' }, 'report.elapsed': { ko: '소요 {d}', en: 'Took {d}' }, 'report.total': { ko: '총 처리', en: 'Total' }, 'report.log': { ko: '로그: {path}', en: 'Log: {path}' }, 'report.errors': { ko: '오류 {n}건 보기', en: 'View {n} errors' }, 'dur.ms': { ko: '{m}분 {s}초', en: '{m}m {s}s' }, 'dur.s': { ko: '{s}초', en: '{s}s' }, // 메뉴 'menu.file': { ko: '파일', en: 'File' }, 'menu.edit': { ko: '편집', en: 'Edit' }, 'menu.view': { ko: '보기', en: 'View' }, 'menu.window': { ko: '창', en: 'Window' }, 'menu.help': { ko: '도움말', en: 'Help' }, 'menu.quit': { ko: '종료', en: 'Quit' }, 'menu.undo': { ko: '실행 취소', en: 'Undo' }, 'menu.redo': { ko: '다시 실행', en: 'Redo' }, 'menu.cut': { ko: '잘라내기', en: 'Cut' }, 'menu.copy': { ko: '복사', en: 'Copy' }, 'menu.paste': { ko: '붙여넣기', en: 'Paste' }, 'menu.selectall': { ko: '모두 선택', en: 'Select All' }, 'menu.reload': { ko: '새로고침', en: 'Reload' }, 'menu.devtools': { ko: '개발자 도구', en: 'Toggle DevTools' }, 'menu.resetzoom': { ko: '실제 크기', en: 'Actual Size' }, 'menu.zoomin': { ko: '확대', en: 'Zoom In' }, 'menu.zoomout': { ko: '축소', en: 'Zoom Out' }, 'menu.fullscreen': { ko: '전체 화면', en: 'Toggle Full Screen' }, 'menu.minimize': { ko: '최소화', en: 'Minimize' }, 'menu.close': { ko: '닫기', en: 'Close' }, 'menu.appearance': { ko: '테마', en: 'Appearance' }, 'menu.theme.dark': { ko: '다크 모드', en: 'Dark' }, 'menu.theme.light': { ko: '라이트 모드', en: 'Light' }, 'menu.language': { ko: '언어', en: 'Language' }, 'menu.guide': { ko: '사용 방법 가이드', en: 'User Guide' }, 'menu.about': { ko: '정보', en: 'About' } } /** 번역. 누락 키는 키 자체를 반환(개발 중 가시성). {token} 치환 지원. */ export function translate( lang: Lang, key: string, params?: Record ): string { const entry = MESSAGES[key] let text = entry ? entry[lang] : key if (params) { for (const [k, v] of Object.entries(params)) { text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)) } } return text }