// 다국어(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.easy': { ko: '화면 크기', en: 'Display size' }, 'onboard.easyOn': { ko: '크게 (쉬운 모드)', en: 'Large (Easy)' }, 'onboard.easyOff': { ko: '보통', en: 'Normal' }, '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' }, // 파일 탐색기 (정리 탭 사이드바) 'explorer.title': { ko: '파일 탐색기', en: 'File Explorer' }, 'explorer.asSource': { ko: '소스로', en: 'As source' }, 'explorer.asOutput': { ko: '출력으로', en: 'As output' }, 'explorer.copy': { ko: '복사', en: 'Copy' }, 'explorer.copied': { ko: '경로 복사됨', en: 'Path copied' }, 'explorer.hint': { ko: '폴더를 끌어서 경로 칸에 놓거나, 다른 폴더 위에 놓아 이동할 수 있어요.', en: 'Drag a folder onto a path field, or onto another folder to move it.' }, 'explorer.newFolder': { ko: '새 폴더', en: 'New folder' }, 'explorer.delete': { ko: '삭제', en: 'Delete' }, 'explorer.newFolderPrompt': { ko: '새 폴더 이름:', en: 'New folder name:' }, 'explorer.confirmDelete': { ko: "'{name}' 폴더를 휴지통으로 보낼까요?", en: "Move folder '{name}' to the Recycle Bin?" }, 'explorer.confirmMove': { ko: "'{src}' 을(를) '{dest}' 안으로 이동할까요?", en: "Move '{src}' into '{dest}'?" }, 'explorer.opFailed': { ko: '작업 실패: {msg}', en: 'Operation failed: {msg}' }, 'explorer.loading': { ko: '드라이브 검색 중…', en: 'Scanning drives…' }, // 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' }, // 내비게이션 / 라이브러리 (Phase 0) 'nav.organize': { ko: '정리', en: 'Organize' }, 'nav.library': { ko: '라이브러리', en: 'Library' }, 'nav.search': { ko: '검색', en: 'Search' }, 'nav.groups': { ko: '그룹·정화', en: 'Groups' }, 'nav.map': { ko: '지도', en: 'Map' }, // 쉬운 모드 대형 네비(구어체) 'easynav.organize': { ko: '사진 정리', en: 'Organize' }, 'easynav.library': { ko: '내 사진', en: 'My Photos' }, 'easynav.search': { ko: '사진 찾기', en: 'Find' }, 'easynav.map': { ko: '지도로 보기', en: 'Map' }, 'easynav.groups': { ko: '중복 정리', en: 'Cleanup' }, // 지도 / 연관 탐색 (Phase C) 'map.section': { ko: '지도 (촬영 장소)', en: 'Map (photo locations)' }, 'map.count': { ko: 'GPS 사진 {n}장', en: '{n} geotagged' }, 'map.empty': { ko: 'GPS 정보가 있는 사진이 없습니다. (라이브러리에서 재색인하면 GPS가 채워집니다)', en: 'No geotagged photos yet. (Re-index in Library to populate GPS)' }, 'map.related': { ko: '연관 사진', en: 'Related photos' }, 'map.relatedHint': { ko: '지도의 사진을 클릭하면 같은 장소·시기·비슷한 장면의 사진을 모아 보여줍니다.', en: 'Click a photo on the map to see ones from the same place, time, and similar scenes.' }, 'map.selectHint': { ko: '지도에서 사진을 클릭하세요.', en: 'Click a photo on the map.' }, 'map.loading': { ko: '불러오는 중…', en: 'Loading…' }, 'map.geotag': { ko: '지오태깅', en: 'Geotagging' }, 'map.geotagHint': { ko: 'GPS가 없는 사진을 지도 위로 끌어다 놓으면 위치가 지정됩니다. (원본 파일은 그대로, 색인에만 저장)', en: 'Drag a photo without GPS onto the map to set its location. (Saved to the index only; original files unchanged.)' }, 'map.untaggedCount': { ko: '위치 없는 사진 {n}장', en: '{n} without location' }, 'map.allTagged': { ko: '모든 사진에 위치가 있습니다.', en: 'All photos already have a location.' }, 'map.tagged': { ko: '위치를 지정했습니다.', en: 'Location set.' }, 'map.dropHere': { ko: '여기에 놓아 위치 지정', en: 'Drop here to set location' }, // 그룹화 / 자가정화 (Phase 3) 'groups.section': { ko: '유사 사진 그룹 · 자가정화', en: 'Similar groups · Cleanup' }, 'groups.hint': { ko: '검색 색인(임베딩)을 기반으로 비슷한 사진을 묶습니다. 각 그룹의 보관 추천(가장 선명)만 남기고 나머지를 휴지통으로 보낼 수 있습니다.', en: 'Groups similar photos using the search embeddings. Keep the recommended (sharpest) one per group and send the rest to the trash.' }, 'groups.threshold': { ko: '유사도 ({v})', en: 'Similarity ({v})' }, 'groups.find': { ko: '그룹 찾기', en: 'Find groups' }, 'groups.finding': { ko: '찾는 중…', en: 'Finding…' }, 'groups.empty': { ko: '유사 그룹이 없습니다. (먼저 검색 탭에서 임베딩 색인을 생성하세요)', en: 'No similar groups. (Build the embedding index in the Search tab first)' }, 'groups.count': { ko: '{n}개 그룹', en: '{n} groups' }, 'groups.keep': { ko: '추천 보관', en: 'Keep' }, 'groups.groupSize': { ko: '유사 {n}장', en: '{n} similar' }, 'groups.trashSelected': { ko: '선택 {n}개 휴지통으로', en: 'Trash {n} selected' }, 'groups.confirmTrash': { ko: '선택한 {n}개 사진을 휴지통(복구 가능)으로 보냅니다. 계속할까요?', en: 'Move {n} selected photos to the trash (recoverable)? Continue?' }, 'groups.trashed': { ko: '{n}개를 휴지통으로 이동했습니다.', en: 'Moved {n} to trash.' }, // 검색 (Phase 2) 'search.section': { ko: '검색 색인', en: 'Search index' }, 'search.hint': { ko: '자연어로 사진을 검색하려면 먼저 CLIP 임베딩 색인을 생성하세요(최초 1회 모델 다운로드 필요).', en: 'Build the CLIP embedding index to search photos by natural language (first run downloads the model).' }, 'search.build': { ko: '검색 색인 생성', en: 'Build search index' }, 'search.cancel': { ko: '취소', en: 'Cancel' }, 'search.building': { ko: '임베딩 중…', en: 'Embedding…' }, 'search.status': { ko: '임베딩 {embedded} / {total}', en: 'Embedded {embedded} / {total}' }, 'search.placeholder': { ko: '예: 푸른 바다, 노을, 강아지가 있는 사진…', en: 'e.g. blue ocean, sunset, photos with a dog…' }, 'search.go': { ko: '검색', en: 'Search' }, 'search.searching': { ko: '검색 중…', en: 'Searching…' }, 'search.noResults': { ko: '결과가 없습니다.', en: 'No results.' }, 'search.prompt': { ko: '검색어를 입력하세요.', en: 'Type a query to search.' }, 'lib.section': { ko: '라이브러리 폴더', en: 'Library Folders' }, 'lib.hint': { ko: '색인할 폴더를 추가하세요. 사진은 옮기지 않고 제자리에서 색인됩니다(비파괴).', en: 'Add folders to index. Photos are indexed in place, never moved (non-destructive).' }, 'lib.add': { ko: '폴더 추가', en: 'Add folder' }, 'lib.empty': { ko: '색인할 라이브러리 폴더가 없습니다.', en: 'No library folders yet.' }, 'lib.remove': { ko: '제거', en: 'Remove' }, 'lib.index': { ko: '색인 시작', en: 'Start indexing' }, 'lib.cancel': { ko: '취소', en: 'Cancel' }, 'lib.indexing': { ko: '색인 중…', en: 'Indexing…' }, 'lib.assets': { ko: '색인된 자산 {n}개', en: '{n} assets indexed' }, 'lib.progress': { ko: '{done} / {total} · 신규 {indexed} · 스킵 {skipped}', en: '{done} / {total} · {indexed} new · {skipped} skipped' }, 'lib.doneSummary': { ko: '완료 — 신규 {indexed} · 스킵 {skipped} · 실패 {failed} · 총 {assets}개', en: 'Done — {indexed} new · {skipped} skipped · {failed} failed · {assets} total' }, 'lib.grid': { ko: '색인된 사진', en: 'Indexed photos' }, 'lib.gridEmpty': { ko: '색인된 사진이 없습니다. 폴더를 추가하고 색인을 실행하세요.', en: 'No indexed photos. Add a folder and run indexing.' }, 'lib.loadMore': { ko: '더 보기', en: 'Load more' }, 'lib.density': { ko: '크기', en: 'Size' }, 'lib.densityHint': { ko: '썸네일 크기 조절 — 작게 하면 폴더 전체를 한눈에(컨택트시트), 크게 하면 자세히 봅니다.', en: 'Thumbnail size — smaller shows the whole folder at a glance, larger shows detail.' }, 'viewer.back': { ko: '뒤로', en: 'Back' }, 'viewer.hint': { ko: '← → 이동 · Esc 닫기', en: '← → navigate · Esc to close' }, 'viewer.videoUnsupported': { ko: '이 영상 형식은 미리보기를 지원하지 않습니다.', en: 'Preview is not available for this video format.' }, // 포토모자이크 (대표 사진을 라이브러리 사진들로 재구성) 'panel.mosaic': { ko: '포토모자이크', en: 'Photo mosaic' }, 'mosaic.make': { ko: '🧩 이 사진으로 모자이크', en: '🧩 Make mosaic' }, 'mosaic.hint': { ko: '대표 사진을 한 번 클릭(선택)한 뒤 버튼을 누르세요. 라이브러리 사진들이 작은 타일이 되어 그 사진을 재현합니다.', en: 'Click a target photo to select it, then press the button. Library photos become tiles that recreate it.' }, 'mosaic.needTarget': { ko: '대표 사진을 먼저 선택하세요.', en: 'Select a target photo first.' }, 'mosaic.target': { ko: '대상', en: 'Target' }, 'mosaic.tiles': { ko: '타일 {n}장', en: '{n} tiles' }, 'mosaic.resolution': { ko: '해상도', en: 'Resolution' }, 'mosaic.blend': { ko: '색 보정', en: 'Color blend' }, 'mosaic.unique': { ko: '중복 줄이기', en: 'Reduce repeats' }, 'mosaic.save': { ko: 'PNG 저장', en: 'Save PNG' }, 'mosaic.building': { ko: '타일 색상 분석 중…', en: 'Analyzing tile colors…' }, 'mosaic.tooFew': { ko: '타일이 너무 적습니다. 색인된 사진이 많을수록 모자이크가 정교해집니다.', en: 'Too few tiles. The more indexed photos, the finer the mosaic.' }, // 선택 / 내보내기 / 삭제 (Library) 'sel.count': { ko: '{n}개 선택', en: '{n} selected' }, 'sel.selectAll': { ko: '전체 선택', en: 'Select all' }, 'sel.clear': { ko: '선택 해제', en: 'Clear' }, 'sel.export': { ko: '폴더로 내보내기', en: 'Export to folder' }, 'sel.delete': { ko: '삭제', en: 'Delete' }, 'sel.hint': { ko: '사진을 클릭해 선택하세요.', en: 'Click photos to select.' }, 'sel.confirmDelete': { ko: '선택한 {n}개를 휴지통으로 보냅니다.\n(원본 파일이 휴지통으로 이동되며 복구할 수 있습니다)\n계속할까요?', en: 'Move {n} selected items to the Recycle Bin?\n(Originals are moved to the trash and can be restored.)\nContinue?' }, 'sel.deleted': { ko: '{n}개를 휴지통으로 이동했습니다.', en: 'Moved {n} to the trash.' }, 'sel.exported': { ko: '{n}개를 내보냈습니다.\n{dest}', en: 'Exported {n} items.\n{dest}' }, // 미디어 종류 (사진/영상 분리) 'media.all': { ko: '전체', en: 'All' }, 'media.image': { ko: '사진', en: 'Photos' }, 'media.video': { ko: '영상', en: 'Videos' }, // 컬링 필터 / 품질 플래그 (Phase 1) 'filter.kind': { ko: '종류', en: 'Type' }, 'filter.quality': { ko: '품질', en: 'Quality' }, 'cull.all': { ko: '전체', en: 'All' }, 'cull.candidate': { ko: '잘나온', en: 'Good' }, 'cull.rejected': { ko: '걸러낼', en: 'Cull' }, 'cull.help': { ko: '흐리거나 눈 감은·노출 나쁜 사진을 자동으로 표시해, 잘 나온 사진만 빠르게 고를 수 있게 도와줍니다.', en: 'Auto-flags blurry / closed-eye / badly-exposed shots so you can quickly keep the good ones.' }, 'flag.candidate': { ko: '좋음', en: 'Good' }, 'flag.blurry': { ko: '흐림', en: 'Blurry' }, 'flag.eyesClosed': { ko: '눈감음', en: 'Eyes closed' }, 'flag.badExposure': { ko: '노출', en: 'Exposure' }, 'cull.thresholds': { ko: '품질 임계값', en: 'Quality thresholds' }, 'cull.focus': { ko: '초점', en: 'Focus' }, 'cull.exposure': { ko: '노출', en: 'Exposure' }, 'cull.eyes': { ko: '눈 뜸', en: 'Eyes open' }, 'cull.thresholdHint': { ko: '값을 올리면 더 엄격하게 제외됩니다. 변경 즉시 재분석 없이 반영됩니다.', en: 'Higher = stricter rejection. Applied instantly without re-analysis.' }, 'cull.reset': { ko: '기본값', en: 'Reset' }, 'cull.ratingMin': { ko: '별점', en: 'Rating' }, // 컬렉션 패싯 (Phase B) 'col.year': { ko: '연도', en: 'Year' }, 'col.camera': { ko: '카메라', en: 'Camera' }, 'col.label': { ko: '색라벨', en: 'Label' }, 'col.folder': { ko: '폴더', en: 'Folder' }, // darktable식 3-패널 (라이브러리 워크스페이스) 'panel.library': { ko: '라이브러리', en: 'Library' }, 'panel.explorer': { ko: '파일 탐색기', en: 'Folders' }, 'panel.collections': { ko: '컬렉션', en: 'Collections' }, 'panel.filters': { ko: '필터', en: 'Filters' }, 'panel.thresholds': { ko: '품질 임계값', en: 'Quality thresholds' }, 'panel.info': { ko: '이미지 정보', en: 'Image information' }, 'panel.selection': { ko: '선택', en: 'Selection' }, 'panel.actions': { ko: '작업', en: 'Actions' }, 'panel.rate': { ko: '평가', en: 'Rating' }, 'panel.tagging': { ko: '태깅', en: 'Tags' }, 'panel.metadata': { ko: '메타데이터', en: 'Metadata' }, 'info.none': { ko: '사진을 클릭하면 정보가 표시됩니다.', en: 'Click a photo to see info.' }, 'info.file': { ko: '파일', en: 'File' }, 'info.date': { ko: '촬영', en: 'Date' }, 'info.camera': { ko: '카메라', en: 'Camera' }, 'info.lens': { ko: '렌즈', en: 'Lens' }, 'info.exposure': { ko: '노출', en: 'Exposure' }, 'info.iso': { ko: 'ISO', en: 'ISO' }, 'info.size': { ko: '크기', en: 'Size' }, 'info.gps': { ko: '위치', en: 'GPS' }, 'info.folder': { ko: '폴더', en: 'Folder' }, 'tag.placeholder': { ko: '태그 입력 후 Enter', en: 'Type a tag, press Enter' }, 'tag.attachHint': { ko: '선택한 사진(없으면 클릭한 사진)에 부착됩니다.', en: 'Attached to the selection (or the focused photo).' }, 'tag.assetTags': { ko: '이 사진의 태그', en: "This photo's tags" }, 'meta.title': { ko: '제목', en: 'Title' }, 'meta.description': { ko: '설명', en: 'Description' }, 'meta.creator': { ko: '작성자', en: 'Creator' }, 'meta.save': { ko: '저장', en: 'Save' }, 'meta.saved': { ko: '저장됨', en: 'Saved' }, 'meta.selectFirst': { ko: '사진을 먼저 클릭하세요.', en: 'Click a photo first.' }, // 메뉴 '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.easyMode': { ko: '쉬운 모드 (큰 화면)', en: 'Easy mode (large)' }, '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 }