227 lines
9.1 KiB
Python
227 lines
9.1 KiB
Python
|
|
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]
|
||
|
|
original_block = entry['original_block']
|
||
|
|
|
||
|
|
old_msgstr_part = f'msgstr "{entry["msgstr"]}"'
|
||
|
|
escaped_new_msgstr = new_msgstr.replace('"', '\\"')
|
||
|
|
new_msgstr_part = f'msgstr "{escaped_new_msgstr}"'
|
||
|
|
|
||
|
|
if old_msgstr_part in original_block:
|
||
|
|
updated_block = original_block.replace(old_msgstr_part, new_msgstr_part)
|
||
|
|
content = content.replace(original_block, updated_block)
|
||
|
|
logging.info(f" > 업데이트: msgctxt='{msgctxt}'")
|
||
|
|
update_count += 1
|
||
|
|
else:
|
||
|
|
logging.warning(f" > 패턴 불일치 (업데이트 실패): msgctxt='{msgctxt}'의 msgstr을 원본에서 찾지 못했습니다.")
|
||
|
|
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()
|