Files
photoai/src/shared/i18n.ts
T
koriweb d2546f9cbf Library workspace upgrades: darkroom viewer, mosaic, geotagging, file explorer
- 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>
2026-06-02 14:47:26 +09:00

438 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 다국어(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.450.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, 23 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
}