Files
DS_L10N/Update TSV to PO.py
2025-09-11 01:12:13 +09:00

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()