
새 기능 시작
RPG 메이커 게임도 번역이 하고 싶어졌다.
RPG Maker MV/MZ 구조 파악
RPG 메이커는 Unity랑 완전히 다르다.
RPGMaker_Game/
├── www/
│ └── data/
│ ├── Map001.json
│ ├── Map002.json
│ ├── CommonEvents.json
│ ├── Actors.json
│ └── ...
JSON이다. 그냥 평문 JSON.
Unity처럼 Bundle 파싱 필요 없다. 그냥 읽으면 된다.
import json
with open('www/data/Map001.json', 'r', encoding='utf-8') as f:
data = json.load(f)
간단하다. 너무 간단해서 의심스럽다.
RPG Maker JSON 구조
{
"id": 1,
"name": "Map001",
"events": [
{
"id": 1,
"pages": [
{
"list": [
{"code": 401, "parameters": ["こんにちは"]},
{"code": 401, "parameters": ["元気ですか?"]},
{"code": 102, "parameters": [["はい", "いいえ"]]}
]
}
]
}
]
}
code: 401= 대사code: 102= 선택지code: 355= 스크립트
코드 번호로 구분. 심플하다.
추출 로직 (Unity보다 쉽다)
# core/rpgmaker_extractor.py
class RPGMakerExtractor:
def extract_from_json(self, json_path):
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
entries = []
# Map 파일
if 'events' in data:
for event in data['events']:
if not event:
continue
for page in event['pages']:
for cmd in page['list']:
# 401 = 대사, 405 = 스크롤 텍스트
if cmd['code'] in [401, 405]:
text = cmd['parameters'][0]
if self._is_japanese(text):
entries.append({
'file': json_path.name,
'original': text,
'code': cmd['code']
})
# 102 = 선택지
elif cmd['code'] == 102:
for choice in cmd['parameters'][0]:
if self._is_japanese(choice):
entries.append({
'file': json_path.name,
'original': choice,
'code': 102
})
# CommonEvents, Actors, Items 등
# ... 비슷한 로직 ...
return entries
Unity보다 훨씬 간단하다. Bundle 파싱도 없고, TypeTree도 없다.
문제 발생: Excel 가져오기 폭사
번역 완료. Excel 내보내기 성공.
Excel 수정 후 가져오기 클릭.
Traceback (most recent call last):
File "gui/handlers/excel_handler.py", line 316
for entry in data.get('entries', []):
AttributeError: 'list' object has no attribute 'get'
터졌다.
원인 파악 (포맷 차이)
Unity 포맷 (dict):
{
"entries": [
{"text": "Hello", "translated": "안녕"},
{"text": "World", "translated": "세계"}
]
}
RPG Maker 포맷 (list):
[
{"id": 1, "original": "こんにちは", "translated": "안녕하세요"},
{"id": 2, "original": "元気ですか", "translated": "잘 지내세요"}
]
JSON 최상위가 dict vs list.
코드는 Unity dict만 가정했다. RPG Maker list는 처리 못 한다.
수정: 포맷 감지 로직
# gui/handlers/excel_handler.py
def _apply_excel_to_json(self, json_path, updates_map):
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
modified = False
# 포맷 감지
if isinstance(data, list):
# RPG Maker 포맷 (list)
for entry in data:
original_text = entry.get('original', '')
if original_text in updates_map:
entry['translated'] = updates_map[original_text]
modified = True
elif isinstance(data, dict) and 'entries' in data:
# Unity 포맷 (dict)
for entry in data.get('entries', []):
original_text = entry.get('text', '')
if original_text in updates_map:
entry['translated'] = updates_map[original_text]
modified = True
if modified:
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return modified
isinstance로 타입 체크. list면 RPG Maker, dict면 Unity.
Excel 가져오기 재시도. 성공.
번역 검수가 불편하다
Excel 워크플로우:
- 번역 완료
- Excel 내보내기
- Excel 열기
- 수정
- Excel 저장
- Excel 가져오기
6단계나 된다.
사용자: "Excel 열지 않고 프로그램에서 바로 검수하면 안 돼요?"
좋은 아이디어다.
번역 검수 뷰어 구현
요구사항
- 페이징 (50개씩)
- 검색 (원문/번역/수정본)
- 테이블에서 직접 수정
- 수정 사항 하이라이트
- 저장 버튼 한 번에 적용
구현
# gui/widgets/translation_viewer.py
class TranslationViewerWidget(QWidget):
entry_modified = pyqtSignal(int, str) # (index, modified_text)
def __init__(self, parent=None):
super().__init__(parent)
self.entries = []
self.filtered_entries = []
self.current_page = 0
self.items_per_page = 50
self.modified_entries = {} # {index: modified_text}
self.init_ui()
def init_ui(self):
# 검색 영역
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("원문 또는 번역문 검색...")
self.search_type_combo = QComboBox()
self.search_type_combo.addItems(["전체", "원문만", "번역만", "수정본만"])
# 테이블
self.table = QTableWidget()
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels([
"번호", "파일", "원문", "AI 번역", "수정본"
])
# 수정본 컬럼만 편집 가능
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
# ... UI 구성 ...
페이징 로직
def update_table(self):
# 현재 페이지 항목
start_idx = self.current_page * self.items_per_page
end_idx = min(start_idx + self.items_per_page, len(self.filtered_entries))
page_entries = self.filtered_entries[start_idx:end_idx]
self.table.setRowCount(len(page_entries))
for row, entry in enumerate(page_entries):
global_idx = self.entries.index(entry)
# 원문
item_original = QTableWidgetItem(entry['original'])
item_original.setFlags(item_original.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 2, item_original)
# AI 번역
item_translated = QTableWidgetItem(entry['translated'])
item_translated.setFlags(item_translated.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 3, item_translated)
# 수정본 (편집 가능)
modified = self.modified_entries.get(global_idx, '')
item_modified = QTableWidgetItem(modified)
item_modified.setFlags(item_modified.flags() | Qt.ItemFlag.ItemIsEditable)
# 수정된 항목 하이라이트
if modified:
item_modified.setBackground(QColor(200, 255, 200)) # 연두색
item_modified.setFont(QFont("맑은 고딕", 9, QFont.Weight.Bold))
else:
item_modified.setBackground(QColor(255, 255, 200)) # 노란색 (편집 대기)
self.table.setItem(row, 4, item_modified)
검색 기능
def on_search(self):
search_text = self.search_input.text().strip().lower()
search_type = self.search_type_combo.currentText()
if not search_text:
self.filtered_entries = self.entries.copy()
else:
self.filtered_entries = []
for entry in self.entries:
original = entry.get('original', '').lower()
translated = entry.get('translated', '').lower()
idx = self.entries.index(entry)
modified = self.modified_entries.get(idx, '').lower()
# 검색 타입별 필터링
match = False
if search_type == "전체":
match = (search_text in original or
search_text in translated or
search_text in modified)
elif search_type == "원문만":
match = search_text in original
elif search_type == "번역만":
match = search_text in translated
elif search_type == "수정본만":
match = search_text in modified
if match:
self.filtered_entries.append(entry)
self.current_page = 0
self.update_view()
UI 통합
# gui/ui/tab_builder.py
def create_excel_tab(self):
layout = QVBoxLayout()
# 버튼 영역
btn_layout = QHBoxLayout()
btn_load = QPushButton("📂 번역 결과 불러오기")
btn_load.clicked.connect(self.load_translation_for_review)
btn_layout.addWidget(btn_load)
btn_export = QPushButton("📤 Excel 내보내기")
btn_export.clicked.connect(self.export_excel)
btn_layout.addWidget(btn_export)
btn_import = QPushButton("📥 수정본 Excel 가져오기")
btn_import.clicked.connect(self.import_excel_to_viewer)
btn_layout.addWidget(btn_import)
btn_save = QPushButton("💾 수정 사항 저장")
btn_save.clicked.connect(self.save_viewer_modifications)
btn_layout.addWidget(btn_save)
layout.addLayout(btn_layout)
# 뷰어 위젯
self.translation_viewer = TranslationViewerWidget()
layout.addWidget(self.translation_viewer)
return layout
워크플로우 개선
Before (Excel 필수)
번역 → Excel 내보내기 → Excel 열기 → 수정 → 저장 → 가져오기
6단계
After (뷰어 사용)
번역 → 뷰어에서 바로 수정 → 저장
3단계
50% 단계 감소.
사용자는 여전히 Excel로도 작업 가능. 선택권 제공.
실제 사용 사례
RPG Maker 게임 번역
- 게임 폴더 선택 (
www/data/자동 감지) - 번역 시작 (Claude API)
- 뷰어에서 검수
- "魔法" 검색 → 관련 대사 전부 확인
- 고유명사 일관성 체크
- 어색한 번역 수정
- 저장 버튼 클릭
- 게임 실행 → 한글 나옴
총 소요 시간: 10분
Unity + Naninovel 게임
이전과 동일하게 작동. 포맷 감지가 자동이라 신경 안 써도 됨.
성과
지원 게임 엔진
✅ Unity (일반)
✅ Unity + Naninovel
✅ RPG Maker MV
✅ RPG Maker MZ
워크플로우
✅ 뷰어 내 검수 (NEW)
✅ Excel 검수 (기존)
✅ 검색/필터 (NEW)
✅ 페이징 50개씩 (NEW)
코드 변경
+ gui/widgets/translation_viewer.py (379줄)
+ gui/widgets/__init__.py (5줄)
~ gui/handlers/excel_handler.py (+포맷 감지 로직)
~ gui/ui/tab_builder.py (뷰어 통합)
배운 것
- 포맷 가정하지 말기
Unity는 dict, RPG Maker는 list. isinstance로 체크. - 게으름이 기능을 만든다
"Excel 말고 프로그램에서 확인하자" → 뷰어 탄생. - 선택권을 주자
Excel 워크플로우 제거 안 함. 뷰어 추가만.
사용자가 선택. 강요 안 함. - 페이징은 필수
5,000개 항목 한 번에? 프로그램 죽음. 50개씩 나누기. - 검색은 생산성
"魔法" 검색 → 고유명사 일관성 한 번에 체크 가능.
'개발관련' 카테고리의 다른 글
| Unity 게임 번역기 개발기 #6: 최적화와 크로스플랫폼 (0) | 2025.10.19 |
|---|---|
| Unity 게임 번역기 개발기 #4: 개발은 디테일이다 (1) | 2025.10.12 |
| Unity 게임 번역기 개발기 #3: 코드가 괴물이 되다 (0) | 2025.10.11 |
| Unity 게임 번역기 개발기 #2: 돈을 쓰기로 했다 (0) | 2025.10.10 |
| Unity 게임 번역기 개발기 #1: 이게 시작이었다 (0) | 2025.10.09 |