import csv import os import logging import re from datetime import datetime from tkinter import filedialog, Tk # --- 설정 --- TARGET_PO_FILE = 'LocalExport.po' LOG_FILE = 'update_po.log' MSGCTXT_COLUMN = 'msgctxt' EXCLUDE_COLUMNS = ['SourceLocation', 'msgid'] # --- 로깅 설정 --- stream_handler = logging.StreamHandler() stream_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] - %(message)s')) logging.basicConfig( level=logging.INFO, handlers=[stream_handler] ) def get_paths_from_user(): """ Tkinter를 사용하여 사용자로부터 입력 파일 경로와 루트 폴더 경로를 받습니다. """ root = Tk() root.withdraw() logging.info("번역 데이터가 포함된 원본 파일을 선택해주세요 (.csv 또는 .txt)") input_file_path = filedialog.askopenfilename( title="번역 원본 파일 선택", filetypes=(("Text files", "*.txt"), ("CSV files", "*.csv"), ("All files", "*.*")) ) if not input_file_path: logging.warning("파일을 선택하지 않았습니다. 스크립트를 종료합니다.") return None, None logging.info(f"선택된 파일: {input_file_path}") logging.info("업데이트할 PO 파일이 들어있는 언어 폴더들의 상위 폴더(루트 폴더)를 선택해주세요.") root_folder_path = filedialog.askdirectory( title="언어 폴더들의 루트 폴더 선택 (예: LocalExport 폴더)" ) if not root_folder_path: logging.warning("폴더를 선택하지 않았습니다. 스크립트를 종료합니다.") return None, None logging.info(f"선택된 루트 폴더: {root_folder_path}") return input_file_path, root_folder_path def parse_po_file(file_path): """ PO 파일을 파싱하여 msgctxt를 키로 하는 딕셔너리를 반환합니다. """ normalized_path = os.path.normpath(file_path) if not os.path.exists(normalized_path): logging.warning(f"PO 파일을 찾을 수 없습니다: {normalized_path}") return None translations = {} try: with open(normalized_path, 'r', encoding='utf-8') as f: content = f.read() pattern = re.compile( r'(?:#.KEY:\s*"(.*)"\s*\n)?' r'msgctxt\s+"((?:[^"\\]|\\.)*)"\s*\n' r'msgid\s+"((?:[^"\\]|\\.)*)"\s*\n' r'msgstr\s+"((?:[^"\\]|\\.)*)"', re.DOTALL ) for match in pattern.finditer(content): msgctxt_key = match.group(1) or match.group(2) msgctxt_value = match.group(2).replace('\\"', '"') msgid_value = match.group(3).replace('\\"', '"') msgstr_value = match.group(4).replace('\\"', '"') translations[msgctxt_value] = { "msgid": msgid_value, "msgstr": msgstr_value, "original_block": match.group(0) } except Exception as e: logging.error(f"PO 파일 파싱 중 오류 발생: {normalized_path} - {e}") return None return translations def update_po_file(file_path, translations_to_update): """ PO 파일의 내용을 업데이트합니다. """ po_data = parse_po_file(file_path) if po_data is None: return 0 update_count = 0 normalized_path = os.path.normpath(file_path) try: with open(normalized_path, 'r', encoding='utf-8') as f: content = f.read() for msgctxt, new_msgstr in translations_to_update.items(): if msgctxt in po_data: entry = po_data[msgctxt] current_msgstr = entry['msgstr'] # 먼저 현재 값과 새 값이 다른지 확인 if current_msgstr == new_msgstr: # 변경사항 없음 - 건너뜀 (로그 출력 안함) continue original_block = entry['original_block'] # 백슬래시와 쌍따옴표를 올바르게 이스케이프 escaped_new_msgstr = new_msgstr.replace('\\', '\\\\').replace('"', '\\"') # msgstr 라인 전체를 교체하는 정규식 updated_block = re.sub( r'^(msgstr\s+)\".*\"$', r'\1"' + escaped_new_msgstr + '"', original_block, flags=re.MULTILINE ) if updated_block != original_block: content = content.replace(original_block, updated_block) logging.info(f" > 업데이트: msgctxt='{msgctxt}'") update_count += 1 else: logging.warning(f" > 정규식 교체 실패: msgctxt='{msgctxt}'") else: logging.warning(f" > msgctxt 찾기 실패: '{msgctxt}'를 PO 파일에서 찾을 수 없습니다.") if update_count > 0: with open(normalized_path, 'w', encoding='utf-8') as f: f.write(content) except Exception as e: logging.error(f"PO 파일 업데이트 중 오류 발생: {normalized_path} - {e}") return 0 return update_count def main(): """ 메인 실행 함수 """ input_file, root_folder = get_paths_from_user() if not input_file or not root_folder: input("\n엔터 키를 눌러 프로그램을 종료하세요...") return log_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), LOG_FILE) file_handler = logging.FileHandler(log_file_path, 'w', encoding='utf-8') file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] - %(message)s')) logging.getLogger().addHandler(file_handler) logging.info("="*50) logging.info("PO 파일 업데이트 스크립트를 시작합니다.") logging.info(f"시작 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logging.info(f"선택된 입력 파일: '{os.path.normpath(input_file)}'") logging.info(f"선택된 루트 폴더: '{os.path.normpath(root_folder)}'") logging.info("="*50) translations_by_lang = {} try: with open(input_file, 'r', encoding='utf-8-sig') as f: # **수정된 부분**: Sniffer 대신, 탭(\t)을 구분자로 명시적으로 지정 reader = csv.DictReader(f, delimiter='\t') # 안전장치: 필드가 1개 이하면 구분자 문제를 경고 if len(reader.fieldnames) <= 1: logging.warning("파일에서 컬럼을 2개 미만으로 감지했습니다. 입력 파일이 탭(tab)으로 구분된 것이 맞는지 확인해주세요.") lang_codes = [h for h in reader.fieldnames if h not in EXCLUDE_COLUMNS and h != MSGCTXT_COLUMN] for lang in lang_codes: translations_by_lang[lang] = {} for row in reader: msgctxt = row.get(MSGCTXT_COLUMN) if not msgctxt: continue for lang in lang_codes: if row.get(lang): translations_by_lang[lang][msgctxt] = row[lang] except Exception as e: logging.error(f"입력 파일 처리 중 오류 발생: {e}") input("\n엔터 키를 눌러 프로그램을 종료하세요...") return logging.info(f"성공적으로 '{os.path.basename(input_file)}' 파일을 읽었습니다.") logging.info(f"제외 컬럼({EXCLUDE_COLUMNS})을 제외한 업데이트 대상 언어: {list(translations_by_lang.keys())}") total_updated_files = 0 total_updated_entries = 0 for lang_code in list(translations_by_lang.keys()): lang_folder_path = os.path.join(root_folder, lang_code) if not os.path.isdir(lang_folder_path): logging.warning(f"언어 폴더를 찾을 수 없습니다: '{os.path.normpath(lang_folder_path)}'. 건너뜁니다.") continue po_file_path = os.path.join(lang_folder_path, TARGET_PO_FILE) if os.path.exists(po_file_path): logging.info(f"\n--- '{os.path.normpath(po_file_path)}' 파일 처리 시작 ---") if lang_code in translations_by_lang and translations_by_lang[lang_code]: update_count = update_po_file(po_file_path, translations_by_lang[lang_code]) if update_count > 0: logging.info(f"'{os.path.normpath(po_file_path)}' 파일에 총 {update_count}개의 항목을 업데이트했습니다.") total_updated_files += 1 total_updated_entries += update_count else: logging.info(f"'{os.path.normpath(po_file_path)}' 파일에 업데이트할 내용이 없거나, 업데이트에 실패했습니다.") else: logging.warning(f"대상 PO 파일을 찾을 수 없습니다: '{os.path.normpath(po_file_path)}'.") logging.info("\n" + "="*50) logging.info("모든 작업이 완료되었습니다.") logging.info(f"총 {total_updated_files}개의 PO 파일을 업데이트했습니다.") logging.info(f"총 {total_updated_entries}개의 번역 항목을 업데이트했습니다.") logging.info(f"종료 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logging.info(f"자세한 내용은 '{os.path.normpath(log_file_path)}' 파일을 확인해주세요.") logging.info("="*50) if __name__ == '__main__': main()