KINDNICK ff71886fd7 v2.7.0: i18n 100% + 런타임 retranslate + 테스트 +47 + 폴리싱
- i18n 사전 100% (break/overtime/leave/clockin) — 50+ 신규 키
- 런타임 재번역 인프라 (ui/i18n_runtime.py) — 재시작 없이 메인 UI 적용
- MealController 분리 — 점심/저녁 토글을 컨트롤러로 추출
- 통합 테스트 +15 (S36-S52: 온보딩/salary/CSV/notification dedupe 등)
- pytest 신규 4종 + i18n_runtime 테스트 (총 122 케이스, 90→122)
- README/INSTALL/CLAUDE/AGENTS v2.6+ 아키텍처 반영
2026-04-30 19:30:47 +09:00

765 lines
34 KiB
Python
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.
`tr('key')` → 현재 언어의 번역 문자열. 키 미존재 시 ko 폴백, ko도 없으면
키 그대로 반환 (미번역도 동작). 점진 도입 가능.
새 언어 추가:
1. _DICT['en'] 옆에 'ja'/'zh' 등 추가
2. 사용자가 설정 → 언어 콤보에서 선택
"""
from __future__ import annotations
_current_lang = 'ko'
_DICT = {
'ko': {
# === 메뉴/버튼 ===
'menu.stats': '통계',
'menu.calendar': '캘린더',
'menu.daily_report': '일일보고',
'menu.help': '도움말',
'menu.settings': '설정',
'btn.clock_out': '퇴근하기',
'btn.clock_out_cancel': '🔄 퇴근 취소',
'btn.lunch_add': '점심시간 추가',
'btn.lunch_applied': '점심시간 (적용됨)',
'btn.dinner_add': '저녁시간 추가',
'btn.dinner_applied': '저녁시간 (적용됨)',
'btn.break_out': '🚪 외출 시작',
'btn.break_in': '↩️ 복귀',
'btn.save': '💾 저장',
'btn.close': '닫기',
'btn.apply': '적용',
'btn.cancel': '취소',
'btn.add': '추가',
'btn.delete': '삭제',
'btn.edit': '편집',
'btn.confirm': '확인',
'btn.ok': '확인',
# === 윈도우/다이얼로그 제목 ===
'window.main_title': '퇴근시간 계산기',
'window.settings': '⚙️ 설정',
'window.help': '📖 사용 설명서',
'window.stats': '📊 근무 통계',
'window.calendar': '📅 캘린더',
'window.mini_widget': '퇴근시간',
'window.clock_in_dialog': '출근 시간',
'window.break_view': '외출 관리',
'window.overtime_view': '연장근무 관리',
'window.leave_view': '연차 관리',
# === 라벨 ===
'label.remaining': '남은 시간',
'label.overtime_progress': '추가 근무 중',
'label.expected_clock_out': '예상 퇴근',
'label.clock_in_time': '출근 시간',
'label.clock_out_time': '퇴근 시간',
'label.work_hours_label': '하루 기본 근무:',
'label.lunch_default': '점심시간 기본:',
'label.dinner_default': '저녁시간 기본:',
'label.work_pattern': '근무 패턴:',
'label.time_format': '시간 형식:',
'label.theme': '테마:',
'label.language': '언어 / Language:',
'label.unit_hour': '시간',
'label.unit_minute': '',
'label.unit_day': '',
'label.unit_count': '',
'label.weekday_mon': '',
'label.weekday_tue': '',
'label.weekday_wed': '',
'label.weekday_thu': '',
'label.weekday_fri': '',
'label.weekday_sat': '',
'label.weekday_sun': '',
'label.am': '오전',
'label.pm': '오후',
# === 알림 ===
'notif.clock_out_soon.title': '⏰ 퇴근 시간 임박',
'notif.clock_out_soon.body': '퇴근까지 {minutes}분 남았습니다.\n마무리 준비를 시작하세요!',
'notif.lunch_reminder.title': '🍱 점심시간 등록',
'notif.lunch_reminder.body': '점심시간을 등록하지 않으셨네요.\n점심시간을 추가하시겠어요?',
'notif.overtime_earning.title': '🔥 연장근무 적립 예정',
'notif.overtime_earning.body': '오늘 {time_str}의 연장근무가 적립될 예정입니다!',
'notif.overtime_threshold.title': '💰 연장근무 적립 알림',
'notif.overtime_threshold.body': '적립된 연장근무가 {hours:.1f}시간 입니다.\n사용을 고려해보세요!',
'notif.health.title': '⚠️ 건강 경고',
'notif.health.body': '{days}일 연속 연장근무 중입니다.\n건강을 챙기세요!',
'notif.weekly_52.title': '🚨 주 52시간 초과',
'notif.weekly_52.body': '이번 주 총 근무시간이 {hours:.1f}시간입니다.\n법정 근로시간을 초과했습니다!',
# === 메시지박스 ===
'msg.save_success.title': '저장 완료',
'msg.save_success.body': '설정이 저장되었습니다.',
'msg.input_error.title': '입력 오류',
'msg.work_min_too_small': '하루 기본 근무 시간은 최소 30분 이상이어야 합니다.',
'msg.no_clock_in.title': '외출 불가',
'msg.no_clock_in.body': '출근하지 않은 상태입니다.',
'msg.already_break.body': '이미 외출 중입니다.',
'msg.no_record.title': '기록 없음',
'msg.no_record.body': '오늘 출근 기록이 없습니다.',
'msg.confirm_delete.title': '삭제 확인',
'msg.no_data.title': '데이터 없음',
'msg.manual_added': '수동 추가',
# === 트레이 ===
'tray.open': '프로그램 열기',
'tray.mini_widget': '📌 미니 위젯',
'tray.toggle_lunch': '🍱 점심시간 토글',
'tray.quit': '종료',
'tray.tooltip_remaining': '퇴근까지: {time}',
'tray.tooltip_overtime': '추가 근무 중: {time}',
'tray.background': '프로그램이 트레이에서 실행 중입니다.',
# === 그룹 박스 ===
'group.work_time': '근무 시간 설정',
'group.notification': '알림 및 표시 설정',
'group.overtime': '연장근무 설정',
'group.leave': '휴가 설정',
'group.holiday': '공휴일 설정',
'group.data': '데이터 관리',
# === StatsView ===
'stats.title': '근무 통계',
'stats.tab_weekly': '주간',
'stats.tab_monthly': '월간',
'stats.tab_pattern': '패턴 분석',
'stats.weekly_summary': '이번 주 요약',
'stats.monthly_summary': '이번 달 요약',
'stats.total_hours': '총 근무시간:',
'stats.work_days': '근무일수:',
'stats.avg_hours': '평균 근무시간:',
'stats.total_overtime': '총 연장근무:',
'stats.pattern_insights': '근무 패턴 인사이트',
'stats.analyzing': '데이터를 분석 중입니다...',
'stats.no_data': '아직 충분한 데이터가 없습니다.',
# === CalendarView ===
'cal.title': '월간 근무 캘린더',
'cal.year_label': '',
'cal.month_label': '',
'cal.prev': '◀ 이전',
'cal.next': '다음 ▶',
'cal.today': '오늘',
'cal.no_record': '기록 없음',
'cal.edit_record': '기록 편집',
# === HelpView (각 탭의 큰 HTML은 별도 키) ===
'help.tab_intro': '👋 시작하기',
'help.tab_work_hours': '🕘 근무시간',
'help.tab_overtime': '🏦 연장근무',
'help.tab_leave': '🌴 연차/휴가',
'help.tab_break': '🚪 외출/저녁',
'help.tab_faq': '❓ 자주 묻는 질문',
# === clock_in_dialog ===
'dlg.clock_in.prompt': '오늘의 출근시간을 입력해주세요',
'dlg.clock_in.label': '출근시간:',
'dlg.clock_in.quick': '빠른 선택:',
'dlg.clock_in.btn_now': '현재',
# === break_view ===
'dlg.break.edit_title': '외출 기록 수정',
'dlg.break.out_label': '외출 시간:',
'dlg.break.in_label': '복귀 시간:',
'dlg.break.reason_label': '사유:',
'view.break.today_title': '오늘의 외출 기록',
'view.break.col_out': '외출 시간',
'view.break.col_in': '복귀 시간',
'view.break.col_duration': '소요 시간',
'view.break.col_reason': '사유',
'view.break.in_progress': '진행중',
'view.break.total_zero': '총 외출 시간: 0분',
'view.break.total_fmt': '총 외출 시간: {h}시간 {m}',
'view.break.total_min_only': '총 외출 시간: {m}',
'view.break.duration_fmt': '{h}시간 {m}',
'view.break.duration_min_only': '{m}',
'view.break.delete_confirm': '이 외출 기록을 삭제하시겠습니까?',
'btn.refresh': '새로고침',
'btn.edit_short': '수정',
'btn.delete_short': '삭제',
# === overtime_view ===
'view.overtime.title': '연장근무 내역',
'view.overtime.balance_zero': '잔액: 0분',
'view.overtime.balance_fmt': '현재 잔액: {h}시간 {m}분 ({total}분)',
'view.overtime.earned_group': '💰 적립 내역',
'view.overtime.used_group': '📤 사용 내역',
'view.overtime.col_date': '날짜',
'view.overtime.col_earned': '적립',
'view.overtime.col_used': '사용',
'view.overtime.col_memo': '메모',
'view.overtime.col_reason': '사유',
'view.overtime.btn_add_earned': ' 수동 적립',
'view.overtime.btn_add_used': ' 수동 사용',
'view.overtime.menu_delete': '❌ 삭제',
'view.overtime.delete_confirm_body': '다음 사용 기록을 삭제하시겠습니까?\n\n날짜: {date}\n시간: {time}\n사유: {reason}',
'view.overtime.manual_earned_title': '추가근무 수동 적립',
'view.overtime.manual_used_title': '추가근무 수동 사용',
'view.overtime.field_date': '날짜:',
'view.overtime.field_time': '시간:',
'view.overtime.field_memo': '메모:',
'view.overtime.field_reason': '사유:',
'view.overtime.unit_hour_suffix': '시간',
'view.overtime.minute_0': '0분',
'view.overtime.minute_30': '30분',
'view.overtime.placeholder_memo': '선택사항',
'view.overtime.placeholder_reason': '예: 개인 사유',
'view.overtime.zero_add_error': '0분은 추가할 수 없습니다.',
'view.overtime.zero_use_error': '0분은 사용할 수 없습니다.',
'view.overtime.balance_short_title': '잔액 부족',
'view.overtime.balance_short_body': '사용 가능한 시간이 부족합니다.\n\n요청: {req_h}시간 {req_m}\n잔액: {bal_h}시간 {bal_m}',
'view.overtime.saved_earned': '{h}시간 {m}분이 적립되었습니다.',
'view.overtime.saved_used': '{h}시간 {m}분이 사용 처리되었습니다.',
# === leave_view ===
'view.leave.title': '연차 관리',
'view.leave.balance_zero': '잔여: 0일',
'view.leave.balance_fmt': '잔여: {days}일 (총 {hours}시간)',
'view.leave.btn_set_balance': '잔여 설정',
'view.leave.used_group': '📤 사용 내역',
'view.leave.col_date': '날짜',
'view.leave.col_type': '구분',
'view.leave.col_used': '사용',
'view.leave.col_reason': '사유',
'view.leave.btn_add': ' 연차 사용 추가',
'view.leave.btn_calendar': '📅 캘린더 보기',
'view.leave.delete_confirm_body': '다음 연차 사용 기록을 삭제하시겠습니까?\n\n날짜: {date}\n구분: {type}\n사용: {days}',
'view.leave.set_title': '연차 시간 설정',
'view.leave.set_prompt': '연차 잔여 시간을 입력하세요 (0.5시간 단위):\n예) 8시간 = 1일, 4시간 = 0.5일(반차), 2시간 = 0.25일, 0.5시간 = 30분',
'view.leave.set_done_title': '설정 완료',
'view.leave.set_done_body': '연차 잔여 개수가 {days}일 ({hours}시간)로 설정되었습니다.',
'view.leave.add_title': '연차 사용 기록 추가',
'view.leave.field_date': '날짜:',
'view.leave.field_type': '구분:',
'view.leave.field_hours': '시간:',
'view.leave.field_reason': '사유:',
'view.leave.type_annual': '연차',
'view.leave.type_half': '반차',
'view.leave.type_quarter': '반반차',
'view.leave.type_hourly': '시간',
'view.leave.placeholder_reason': '예) 개인 사유, 병원 방문 등',
'view.leave.note_auto_deduct': '※ 잔여 연차가 자동 차감됩니다.',
'view.leave.short_title': '잔여 연차 부족',
'view.leave.short_body': '잔여 연차가 부족합니다.\n현재 잔여: {balance}\n사용 요청: {req}',
'view.leave.confirm_title': '연차 사용 기록 추가',
'view.leave.confirm_body': '날짜: {date}\n구분: {type}\n사용: {days}일 ({hours}시간)\n사유: {reason}\n\n이 기록을 추가하시겠습니까?',
'view.leave.added_title': '추가 완료',
'view.leave.added_body': '{days}일 ({hours}시간)의 연차 사용이 기록되었습니다.',
'view.leave.error_title': '오류',
'view.leave.error_body': '연차 기록 추가 중 오류가 발생했습니다:\n{err}',
},
'en': {
# === Menu/Buttons ===
'menu.stats': 'Stats',
'menu.calendar': 'Calendar',
'menu.daily_report': 'Daily Report',
'menu.help': 'Help',
'menu.settings': 'Settings',
'btn.clock_out': 'Clock Out',
'btn.clock_out_cancel': '🔄 Cancel Clock-out',
'btn.lunch_add': 'Add Lunch',
'btn.lunch_applied': 'Lunch (Applied)',
'btn.dinner_add': 'Add Dinner',
'btn.dinner_applied': 'Dinner (Applied)',
'btn.break_out': '🚪 Start Break',
'btn.break_in': '↩️ Return',
'btn.save': '💾 Save',
'btn.close': 'Close',
'btn.apply': 'Apply',
'btn.cancel': 'Cancel',
'btn.add': 'Add',
'btn.delete': 'Delete',
'btn.edit': 'Edit',
'btn.confirm': 'Confirm',
'btn.ok': 'OK',
# === Windows ===
'window.main_title': 'Clock-out Time Calculator',
'window.settings': '⚙️ Settings',
'window.help': '📖 User Guide',
'window.stats': '📊 Statistics',
'window.calendar': '📅 Calendar',
'window.mini_widget': 'Clock-out',
'window.clock_in_dialog': 'Clock-in Time',
'window.break_view': 'Break Management',
'window.overtime_view': 'Overtime Management',
'window.leave_view': 'Leave Management',
# === Labels ===
'label.remaining': 'Remaining',
'label.overtime_progress': 'Overtime',
'label.expected_clock_out': 'Expected Clock-out',
'label.clock_in_time': 'Clock-in Time',
'label.clock_out_time': 'Clock-out Time',
'label.work_hours_label': 'Daily work:',
'label.lunch_default': 'Lunch break:',
'label.dinner_default': 'Dinner break:',
'label.work_pattern': 'Work pattern:',
'label.time_format': 'Time format:',
'label.theme': 'Theme:',
'label.language': 'Language / 언어:',
'label.unit_hour': 'h',
'label.unit_minute': 'min',
'label.unit_day': 'day(s)',
'label.unit_count': '',
'label.weekday_mon': 'Mon',
'label.weekday_tue': 'Tue',
'label.weekday_wed': 'Wed',
'label.weekday_thu': 'Thu',
'label.weekday_fri': 'Fri',
'label.weekday_sat': 'Sat',
'label.weekday_sun': 'Sun',
'label.am': 'AM',
'label.pm': 'PM',
# === Notifications ===
'notif.clock_out_soon.title': '⏰ Clock-out Soon',
'notif.clock_out_soon.body': "{minutes} minutes until clock-out.\nWrap things up!",
'notif.lunch_reminder.title': '🍱 Lunch Reminder',
'notif.lunch_reminder.body': "You haven't registered lunch yet.\nWant to add it?",
'notif.overtime_earning.title': '🔥 Overtime Will Accrue',
'notif.overtime_earning.body': "{time_str} of overtime will be banked today!",
'notif.overtime_threshold.title': '💰 Overtime Balance High',
'notif.overtime_threshold.body': "{hours:.1f} hours of overtime banked.\nConsider using some.",
'notif.health.title': '⚠️ Health Warning',
'notif.health.body': "{days} consecutive days of overtime.\nTake care of your health!",
'notif.weekly_52.title': '🚨 Weekly 52h Exceeded',
'notif.weekly_52.body': "This week's total work hours: {hours:.1f}\nLegal limit exceeded!",
# === Message Boxes ===
'msg.save_success.title': 'Saved',
'msg.save_success.body': 'Settings saved successfully.',
'msg.input_error.title': 'Input Error',
'msg.work_min_too_small': 'Daily work time must be at least 30 minutes.',
'msg.no_clock_in.title': 'Cannot Take Break',
'msg.no_clock_in.body': 'Not clocked in.',
'msg.already_break.body': 'Already on break.',
'msg.no_record.title': 'No Record',
'msg.no_record.body': 'No clock-in record for today.',
'msg.confirm_delete.title': 'Confirm Delete',
'msg.no_data.title': 'No Data',
'msg.manual_added': 'Manual',
# === Tray ===
'tray.open': 'Open Program',
'tray.mini_widget': '📌 Mini Widget',
'tray.toggle_lunch': '🍱 Toggle Lunch',
'tray.quit': 'Quit',
'tray.tooltip_remaining': 'Until clock-out: {time}',
'tray.tooltip_overtime': 'Overtime: {time}',
'tray.background': 'Program is running in the tray.',
# === Groups ===
'group.work_time': 'Work Time Settings',
'group.notification': 'Notifications & Display',
'group.overtime': 'Overtime Settings',
'group.leave': 'Leave Settings',
'group.holiday': 'Holidays',
'group.data': 'Data Management',
# === StatsView ===
'stats.title': 'Work Statistics',
'stats.tab_weekly': 'Weekly',
'stats.tab_monthly': 'Monthly',
'stats.tab_pattern': 'Patterns',
'stats.weekly_summary': 'This Week',
'stats.monthly_summary': 'This Month',
'stats.total_hours': 'Total hours:',
'stats.work_days': 'Work days:',
'stats.avg_hours': 'Avg hours/day:',
'stats.total_overtime': 'Total overtime:',
'stats.pattern_insights': 'Work Pattern Insights',
'stats.analyzing': 'Analyzing...',
'stats.no_data': 'Not enough data yet.',
# === CalendarView ===
'cal.title': 'Monthly Calendar',
'cal.year_label': 'Y',
'cal.month_label': 'M',
'cal.prev': '◀ Prev',
'cal.next': 'Next ▶',
'cal.today': 'Today',
'cal.no_record': 'No record',
'cal.edit_record': 'Edit record',
# === HelpView ===
'help.tab_intro': '👋 Getting Started',
'help.tab_work_hours': '🕘 Work Hours',
'help.tab_overtime': '🏦 Overtime',
'help.tab_leave': '🌴 Leave',
'help.tab_break': '🚪 Break/Dinner',
'help.tab_faq': '❓ FAQ',
# === clock_in_dialog ===
'dlg.clock_in.prompt': "Enter today's clock-in time",
'dlg.clock_in.label': 'Clock-in time:',
'dlg.clock_in.quick': 'Quick pick:',
'dlg.clock_in.btn_now': 'Now',
# === break_view ===
'dlg.break.edit_title': 'Edit Break Record',
'dlg.break.out_label': 'Break out:',
'dlg.break.in_label': 'Return:',
'dlg.break.reason_label': 'Reason:',
'view.break.today_title': "Today's Break Records",
'view.break.col_out': 'Break out',
'view.break.col_in': 'Return',
'view.break.col_duration': 'Duration',
'view.break.col_reason': 'Reason',
'view.break.in_progress': 'In progress',
'view.break.total_zero': 'Total break: 0 min',
'view.break.total_fmt': 'Total break: {h}h {m}m',
'view.break.total_min_only': 'Total break: {m} min',
'view.break.duration_fmt': '{h}h {m}m',
'view.break.duration_min_only': '{m} min',
'view.break.delete_confirm': 'Delete this break record?',
'btn.refresh': 'Refresh',
'btn.edit_short': 'Edit',
'btn.delete_short': 'Delete',
# === overtime_view ===
'view.overtime.title': 'Overtime History',
'view.overtime.balance_zero': 'Balance: 0 min',
'view.overtime.balance_fmt': 'Current balance: {h}h {m}m ({total} min)',
'view.overtime.earned_group': '💰 Earned',
'view.overtime.used_group': '📤 Used',
'view.overtime.col_date': 'Date',
'view.overtime.col_earned': 'Earned',
'view.overtime.col_used': 'Used',
'view.overtime.col_memo': 'Memo',
'view.overtime.col_reason': 'Reason',
'view.overtime.btn_add_earned': ' Manual Earn',
'view.overtime.btn_add_used': ' Manual Use',
'view.overtime.menu_delete': '❌ Delete',
'view.overtime.delete_confirm_body': 'Delete this usage record?\n\nDate: {date}\nTime: {time}\nReason: {reason}',
'view.overtime.manual_earned_title': 'Manual Overtime Earn',
'view.overtime.manual_used_title': 'Manual Overtime Use',
'view.overtime.field_date': 'Date:',
'view.overtime.field_time': 'Time:',
'view.overtime.field_memo': 'Memo:',
'view.overtime.field_reason': 'Reason:',
'view.overtime.unit_hour_suffix': 'h',
'view.overtime.minute_0': '0 min',
'view.overtime.minute_30': '30 min',
'view.overtime.placeholder_memo': 'Optional',
'view.overtime.placeholder_reason': 'e.g., personal',
'view.overtime.zero_add_error': 'Cannot add 0 minutes.',
'view.overtime.zero_use_error': 'Cannot use 0 minutes.',
'view.overtime.balance_short_title': 'Insufficient Balance',
'view.overtime.balance_short_body': 'Not enough balance.\n\nRequested: {req_h}h {req_m}m\nBalance: {bal_h}h {bal_m}m',
'view.overtime.saved_earned': '{h}h {m}m banked.',
'view.overtime.saved_used': '{h}h {m}m used.',
# === leave_view ===
'view.leave.title': 'Leave Management',
'view.leave.balance_zero': 'Balance: 0 days',
'view.leave.balance_fmt': 'Balance: {days} days ({hours}h total)',
'view.leave.btn_set_balance': 'Set Balance',
'view.leave.used_group': '📤 Used',
'view.leave.col_date': 'Date',
'view.leave.col_type': 'Type',
'view.leave.col_used': 'Used',
'view.leave.col_reason': 'Reason',
'view.leave.btn_add': ' Add Leave Usage',
'view.leave.btn_calendar': '📅 Calendar',
'view.leave.delete_confirm_body': 'Delete this leave record?\n\nDate: {date}\nType: {type}\nUsed: {days}',
'view.leave.set_title': 'Set Leave Hours',
'view.leave.set_prompt': 'Enter leave hours remaining (0.5h step):\ne.g. 8h = 1d, 4h = 0.5d (half), 2h = 0.25d, 0.5h = 30min',
'view.leave.set_done_title': 'Saved',
'view.leave.set_done_body': 'Leave balance set to {days} days ({hours}h).',
'view.leave.add_title': 'Add Leave Usage',
'view.leave.field_date': 'Date:',
'view.leave.field_type': 'Type:',
'view.leave.field_hours': 'Hours:',
'view.leave.field_reason': 'Reason:',
'view.leave.type_annual': 'Annual',
'view.leave.type_half': 'Half',
'view.leave.type_quarter': 'Quarter',
'view.leave.type_hourly': 'Hourly',
'view.leave.placeholder_reason': 'e.g., personal, medical',
'view.leave.note_auto_deduct': '※ Leave balance is auto-deducted.',
'view.leave.short_title': 'Insufficient Leave',
'view.leave.short_body': 'Not enough leave.\nCurrent: {balance} days\nRequested: {req} days',
'view.leave.confirm_title': 'Add Leave Usage',
'view.leave.confirm_body': 'Date: {date}\nType: {type}\nUsed: {days} days ({hours}h)\nReason: {reason}\n\nAdd this record?',
'view.leave.added_title': 'Added',
'view.leave.added_body': '{days} days ({hours}h) of leave usage recorded.',
'view.leave.error_title': 'Error',
'view.leave.error_body': 'Failed to add leave record:\n{err}',
},
}
# === HelpView 큰 HTML 콘텐츠 (별도 사전) ===
_HELP_HTML = {
'ko': {
'help.html.intro': """
<h2>👋 환영합니다!</h2>
<p><b>퇴근시간 계산기</b>는 출근 시간 자동 감지부터 연장근무 적립·사용까지
하루 근무를 정리해 주는 데스크톱 앱입니다.</p>
<h3>한눈에 보는 기본 흐름</h3>
<ol>
<li><b>출근</b> — 컴퓨터 켜진 시각이 자동으로 출근 시간으로 기록돼요.</li>
<li><b>근무 중</b> — 메인 화면에 퇴근까지 남은 시간이 1초마다 갱신됩니다.</li>
<li><b>점심/저녁</b> — 식사 시간 버튼을 누르면 그만큼 근무시간이 늘어납니다.</li>
<li><b>외출</b> — "외출 시작/복귀" 버튼으로 잠깐 자리 비운 시간을 추적합니다.</li>
<li><b>퇴근</b> — 퇴근 버튼을 누르면 연장근무가 30분 단위로 자동 적립됩니다.</li>
</ol>
<h3>처음 켰다면 꼭 확인하세요</h3>
<ul>
<li><b>설정 → 근무 시간</b>: 본인 근무 패턴(8시간 / 단축 7h30m / 6시간 등)을
프리셋에서 선택하거나 직접 입력하세요.</li>
<li><b>설정 → 휴가 → 연간 연차</b>: 본인 연차 일수를 맞춰 두면 잔여 연차가
자동 계산됩니다.</li>
<li><b>관리자 권한</b>이 필요할 수 있어요. 부팅 시간이 자동 감지되지 않으면
관리자 권한으로 실행해 보세요.</li>
</ul>
""",
'help.html.work_hours': """
<h2>🕘 근무시간 설정</h2>
<h3>표준 근무 / 단축근무 / 시간제 모두 지원</h3>
<ul>
<li><b>표준 8시간</b> — 점심 60분 (기본값)</li>
<li><b>단축근무 7시간 30분</b> — 점심 30분</li>
<li><b>단축근무 7시간</b> — 점심 60분</li>
<li><b>단축근무 6시간</b> — 점심 30분</li>
<li><b>반일 4시간</b> — 점심 없음</li>
<li><b>사용자 정의</b> — 시간/분 직접 입력 (5분 단위)</li>
</ul>
<h3>💡 단축근무 사용자 안내</h3>
<p>예) 하루 7시간 30분 근무 + 점심 30분</p>
<ol>
<li>설정 → 근무 시간 → <b>근무 패턴</b>에서
<i>"단축근무 7시간 30분 (점심 30분)"</i> 선택</li>
<li>또는 직접 입력: <b>하루 기본 근무</b>에 <code>7 시간 30 분</code>,
<b>점심시간 기본</b>에 <code>30 분</code></li>
<li><b>저장</b> 클릭하면 즉시 메인 화면 계산이 갱신됩니다.</li>
</ol>
<h3>점심시간 자동 적용</h3>
<p>설정에서 <b>"자동 적용"</b>을 체크하면 출근 후 <b>4시간 경과</b> 시
점심시간이 자동으로 켜집니다.</p>
""",
'help.html.overtime': """
<h2>🏦 연장근무 30분 단위 적립 시스템</h2>
<h3>적립 규칙</h3>
<p>정규 퇴근시간 이후 일한 시간은 <b>30분 단위로 절삭</b>되어 적립됩니다.</p>
<ul>
<li>1시간 35분 일했다면 → <b>1시간 30분</b> 적립</li>
<li>55분 일했다면 → <b>30분</b> 적립</li>
<li>29분 일했다면 → <b>0분</b> 적립</li>
</ul>
<h3>사용 방법</h3>
<p>적립된 연장근무는 메인 화면 <b>"30분 사용" / "1시간 사용"</b> 버튼으로
쓸 수 있어요. 사용한 만큼 그날 퇴근시간이 앞당겨집니다.</p>
<h3>주말·공휴일 근무</h3>
<p>주말 또는 등록된 공휴일에 일한 시간은
<b>모든 시간이 연장근무로 적립</b>됩니다.</p>
""",
'help.html.leave': """
<h2>🌴 연차·반차 관리</h2>
<h3>연차 잔액 자동 계산</h3>
<p>잔액 = <b>연간 연차</b> (프로그램 외 사용분 + 프로그램에서 기록된 사용분)</p>
<h3>반차·반반차 지원</h3>
<ul>
<li><b>1.0일</b> — 종일 연차</li>
<li><b>0.5일</b> — 반차 (4시간)</li>
<li><b>0.25일</b> — 반반차 (2시간)</li>
</ul>
<h3>단축근무자의 연차 환산</h3>
<p>1일 연차의 시간 길이는 설정의 <b>하루 기본 근무</b>를 따릅니다.
예: 7시간 30분 근무자는 1일 연차가 7시간 30분(=450분)으로 환산됩니다.</p>
""",
'help.html.break': """
<h2>🚪 외출 / 저녁시간</h2>
<h3>외출 (잠깐 자리 비움)</h3>
<p>병원, 잠깐 외근 등으로 자리를 비울 때 <b>외출 시작 → 복귀</b> 버튼으로
시간을 추적하세요.</p>
<h3>화면 잠금 자동 외출</h3>
<p>설정에서 <b>"화면 잠금 시 자동 외출/복귀"</b>를 켜면 PC 잠금 시 자동으로
외출이 시작되고, 풀리면 복귀로 처리됩니다.</p>
<h3>저녁시간</h3>
<p>야근하면서 저녁을 먹는다면 <b>저녁시간 추가</b> 버튼을 눌러주세요.</p>
""",
'help.html.faq': """
<h2>❓ 자주 묻는 질문</h2>
<h3>Q. 출근 시간이 잘못 잡혔어요</h3>
<p>메인 화면 출근 시각 옆 <b>편집(연필)</b> 아이콘으로 수정할 수 있어요.</p>
<h3>Q. 단축근무 7시간 30분으로 설정하고 싶어요</h3>
<p>설정 → 근무 시간 → 근무 패턴에서 프리셋을 선택하거나 시·분을 직접 입력하세요.</p>
<h3>Q. 데이터는 어디에 저장되나요?</h3>
<p>실행 폴더의 <code>database.db</code> (SQLite). 자동 백업은
<code>~/.clockout_backups/</code>에 1일 1회 회전됩니다.</p>
""",
},
'en': {
'help.html.intro': """
<h2>👋 Welcome!</h2>
<p><b>Clock-out Time Calculator</b> is a desktop app that organizes your daily work
— from auto-detecting clock-in time to banking and using overtime.</p>
<h3>Basic Flow</h3>
<ol>
<li><b>Clock in</b> — System boot time is auto-recorded as your clock-in.</li>
<li><b>Working</b> — Remaining time updates every second on the main screen.</li>
<li><b>Lunch/Dinner</b> — Press the meal buttons to extend work time.</li>
<li><b>Break</b> — Track time away with "Start Break / Return" buttons.</li>
<li><b>Clock out</b> — Press the button; overtime is banked in 30-min units.</li>
</ol>
<h3>First-Time Setup</h3>
<ul>
<li><b>Settings → Work Time</b>: pick your pattern from presets
(8h / 7h30m / 6h / 4h half-day) or enter custom values.</li>
<li><b>Settings → Leave</b>: set your annual leave days.</li>
<li><b>Admin rights</b> may be required for boot-time auto-detection.</li>
</ul>
""",
'help.html.work_hours': """
<h2>🕘 Work Time Settings</h2>
<h3>Supports Standard / Reduced / Part-time</h3>
<ul>
<li><b>Standard 8h</b> — 60-min lunch (default)</li>
<li><b>Reduced 7h30m</b> — 30-min lunch</li>
<li><b>Reduced 7h</b> — 60-min lunch</li>
<li><b>Reduced 6h</b> — 30-min lunch</li>
<li><b>Half-day 4h</b> — no lunch</li>
<li><b>Custom</b> — direct input (5-min granularity)</li>
</ul>
<h3>💡 For Reduced-Hours Users</h3>
<p>Example: 7h30m daily + 30-min lunch</p>
<ol>
<li>Settings → Work Time → pick <i>"Reduced 7h30m (30-min lunch)"</i> preset</li>
<li>Or enter directly: 7h 30min daily, 30min lunch</li>
<li>Click <b>Save</b> — main screen recalculates immediately.</li>
</ol>
<h3>Auto Lunch</h3>
<p>Enable "Auto Apply" in settings to automatically turn on lunch
after 4 hours from clock-in.</p>
""",
'help.html.overtime': """
<h2>🏦 30-Minute Overtime Banking</h2>
<h3>Banking Rule</h3>
<p>Time worked past your scheduled clock-out is <b>truncated to 30-min units</b>.</p>
<ul>
<li>1h35m worked → <b>1h30m</b> banked</li>
<li>55min worked → <b>30min</b> banked</li>
<li>29min worked → <b>0min</b> banked</li>
</ul>
<h3>Using Banked Overtime</h3>
<p>Use buttons on the main screen to consume banked overtime.
Each unit lets you clock out earlier on a chosen day.</p>
<h3>Weekend/Holiday Work</h3>
<p>All hours worked on weekends or registered holidays are
<b>banked entirely as overtime</b>.</p>
""",
'help.html.leave': """
<h2>🌴 Annual Leave Management</h2>
<h3>Auto Balance Calculation</h3>
<p>Balance = annual leave (pre-program usage + recorded usage)</p>
<h3>Half / Quarter Day</h3>
<ul>
<li><b>1.0 day</b> — full leave</li>
<li><b>0.5 day</b> — half (4h)</li>
<li><b>0.25 day</b> — quarter (2h)</li>
</ul>
<h3>Reduced-Hours Conversion</h3>
<p>1 leave day equals your configured daily work time.
Example: 7h30m worker → 1 day = 7h30m (450 min).</p>
""",
'help.html.break': """
<h2>🚪 Break / Dinner</h2>
<h3>Break (Briefly Away)</h3>
<p>For short absences (medical, errands), use <b>Start Break / Return</b>.</p>
<h3>Auto Break on Screen Lock</h3>
<p>Enable "Auto break on screen lock" in settings — when the PC locks,
a break starts automatically; on unlock, you return.</p>
<h3>Dinner</h3>
<p>For overtime with dinner, press <b>Add Dinner</b>.</p>
""",
'help.html.faq': """
<h2>❓ FAQ</h2>
<h3>Q. Clock-in time is wrong</h3>
<p>Click the pencil icon next to clock-in time on the main screen to edit.</p>
<h3>Q. How to set 7h30m work day?</h3>
<p>Settings → Work Time → pick the preset or enter hours/minutes directly.</p>
<h3>Q. Where is data stored?</h3>
<p><code>database.db</code> in the program folder (SQLite). Daily auto-backups
rotate in <code>~/.clockout_backups/</code>.</p>
""",
},
}
def set_language(lang: str) -> None:
global _current_lang
if lang in _DICT:
_current_lang = lang
def get_language() -> str:
return _current_lang
def tr(key: str, **kwargs) -> str:
"""번역 조회. 키 미존재 시 ko 폴백 → 키 그대로."""
table = _DICT.get(_current_lang, _DICT['ko'])
text = table.get(key) or _DICT['ko'].get(key) or key
if kwargs:
try:
return text.format(**kwargs)
except (KeyError, IndexError):
return text
return text
def tr_html(key: str) -> str:
"""HTML 콘텐츠 조회 (HelpView용)."""
table = _HELP_HTML.get(_current_lang, _HELP_HTML['ko'])
return table.get(key) or _HELP_HTML['ko'].get(key) or f"<p>missing: {key}</p>"
def available_languages() -> list:
return list(_DICT.keys())
def language_label(code: str) -> str:
return {'ko': '한국어', 'en': 'English'}.get(code, code)