d2546f9cbf
- darkroom-style fullscreen viewer: left info panel, rating/color-label toolbar, bottom filmstrip, keyboard nav (Esc / arrows), placeholder on load failure - thumbnail density slider (contact-sheet) + photo mosaic generator (target -> tiles) - lighttable-style hover info preview (no click needed) - map drag & drop geotagging (saved to index only; originals untouched) - file explorer: parallel drive scan + timeout, create/delete(trash)/move folders; index reparent on move and cleanup on delete (single source of truth) - library: photos-before-videos ordering; drag range select/deselect; native image drag disabled so sweep-select works - responsive sidebar font scaling; no-wrap filter labels; media protocol CORS + video Range Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
438 lines
23 KiB
TypeScript
438 lines
23 KiB
TypeScript
// 다국어(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<Lang, string> = {
|
||
ko: '한국어',
|
||
en: 'English'
|
||
}
|
||
|
||
/** 키 → 언어별 문자열. {token} 형태의 치환자를 지원. */
|
||
type Table = Record<string, Record<Lang, string>>
|
||
|
||
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, string | number>
|
||
): 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
|
||
}
|