Files
photoai/src/shared/i18n.ts
T
koriweb 6dce580846 Add video sorting, reference thumbnails, theme/i18n, menu, DnD/paste, presets
Feature work on top of the initial organizer:

- Videos: .mp4/.mov/.avi/.mkv/.webm/.m4v sorted into output/Movie/YYYY/MM
- Profiles: reference-image thumbnail cards via a secure photoai-media:// protocol
  (serves only registered reference images); per-thumbnail delete; add via
  click, drag & drop, or paste (Ctrl+V) using webUtils.getPathForFile + a
  byte-based addReferenceData path for clipboard images
- Presets: local person library (max 5) saved to userData; one-click apply into
  active profiles; reusing stored descriptors (no recompute)
- Theme: dark/light with dark default (Tailwind class strategy)
- i18n: ko/en table-based localization; first-run onboarding to pick
  language + theme; English-neutral "Unsorted" folder (was [미정])
- App menu: File/Edit/View/Window/Help, localized; Help opens a detailed,
  theme-aware user guide window
- UI: History block scrolls internally (no whole-window scroll);
  threshold/concurrency tooltips; generic example name
- Settings persisted to userData/settings.json; menu + renderer kept in sync
- Docs: NextGen Photo AI feasibility review + Phase 0/1 roadmap

All typecheck/tests (12) /build green; boot smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:40:44 +09:00

188 lines
8.7 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.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.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' },
// 메뉴
'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, 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
}