번역툴 2.0 업데이트
This commit is contained in:
284
lib/validator.py
Normal file
284
lib/validator.py
Normal file
@ -0,0 +1,284 @@
|
||||
"""
|
||||
Translation Validator for DS_L10N
|
||||
번역 검증 시스템
|
||||
"""
|
||||
import re
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ValidationLevel(Enum):
|
||||
"""검증 레벨"""
|
||||
ERROR = 'ERROR' # 치명적 오류
|
||||
WARNING = 'WARNING' # 경고
|
||||
INFO = 'INFO' # 정보
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""검증 이슈"""
|
||||
level: ValidationLevel
|
||||
msgctxt: str
|
||||
lang: str
|
||||
category: str
|
||||
message: str
|
||||
original: str
|
||||
translation: str
|
||||
|
||||
def __str__(self):
|
||||
icon = {
|
||||
ValidationLevel.ERROR: '❌',
|
||||
ValidationLevel.WARNING: '⚠️',
|
||||
ValidationLevel.INFO: 'ℹ️'
|
||||
}[self.level]
|
||||
|
||||
return (f"{icon} [{self.level.value}] {self.lang}: {self.msgctxt}\n"
|
||||
f" 분류: {self.category}\n"
|
||||
f" 문제: {self.message}\n"
|
||||
f" 원문: {self.original}\n"
|
||||
f" 번역: {self.translation}")
|
||||
|
||||
|
||||
class TranslationValidator:
|
||||
"""번역 검증기"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.validation_config = config.get('validation', {})
|
||||
|
||||
# 변수 패턴 컴파일
|
||||
self.variable_pattern = re.compile(r'\{[A-Za-z0-9_]+\}')
|
||||
|
||||
# 리치 텍스트 태그 패턴
|
||||
self.tag_patterns = config.get('rich_text', {}).get('tag_patterns', [])
|
||||
|
||||
def validate_entry(self, msgctxt: str, original: str, translation: str, lang: str) -> List[ValidationIssue]:
|
||||
"""단일 항목 검증"""
|
||||
issues = []
|
||||
|
||||
# 빈 번역 확인
|
||||
if self.validation_config.get('check_empty_translations', True):
|
||||
if not translation or not translation.strip():
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.WARNING,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='빈 번역',
|
||||
message='번역문이 비어있습니다',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
return issues # 빈 번역이면 다른 검증 건너뛰기
|
||||
|
||||
# 변수 일치 확인
|
||||
if self.validation_config.get('check_variables', True):
|
||||
var_issues = self._check_variables(msgctxt, original, translation, lang)
|
||||
issues.extend(var_issues)
|
||||
|
||||
# 리치 텍스트 태그 확인
|
||||
if self.validation_config.get('check_rich_text_tags', True):
|
||||
tag_issues = self._check_rich_text_tags(msgctxt, original, translation, lang)
|
||||
issues.extend(tag_issues)
|
||||
|
||||
# 줄바꿈 확인
|
||||
if self.validation_config.get('check_newlines', True):
|
||||
newline_issues = self._check_newlines(msgctxt, original, translation, lang)
|
||||
issues.extend(newline_issues)
|
||||
|
||||
# 최대 길이 확인
|
||||
max_length = self.validation_config.get('max_length_warning', 200)
|
||||
if len(translation) > max_length:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.INFO,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='길이 초과',
|
||||
message=f'번역문이 {max_length}자를 초과합니다 (현재: {len(translation)}자)',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_variables(self, msgctxt: str, original: str, translation: str, lang: str) -> List[ValidationIssue]:
|
||||
"""변수 일치 확인"""
|
||||
issues = []
|
||||
|
||||
orig_vars = set(self.variable_pattern.findall(original))
|
||||
trans_vars = set(self.variable_pattern.findall(translation))
|
||||
|
||||
# 누락된 변수
|
||||
missing_vars = orig_vars - trans_vars
|
||||
if missing_vars:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.ERROR,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='변수 누락',
|
||||
message=f'누락된 변수: {", ".join(sorted(missing_vars))}',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
# 추가된 변수
|
||||
extra_vars = trans_vars - orig_vars
|
||||
if extra_vars:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.WARNING,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='추가 변수',
|
||||
message=f'원문에 없는 변수: {", ".join(sorted(extra_vars))}',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_rich_text_tags(self, msgctxt: str, original: str, translation: str, lang: str) -> List[ValidationIssue]:
|
||||
"""리치 텍스트 태그 일치 확인"""
|
||||
issues = []
|
||||
|
||||
# 여는 태그와 닫는 태그 개수 확인
|
||||
for tag in self.tag_patterns:
|
||||
if tag == '</>':
|
||||
continue # 닫는 태그는 별도 처리
|
||||
|
||||
orig_count = original.count(tag)
|
||||
trans_count = translation.count(tag)
|
||||
|
||||
if orig_count != trans_count:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.ERROR,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='태그 불일치',
|
||||
message=f'태그 {tag} 개수 불일치 (원문: {orig_count}, 번역: {trans_count})',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
# 닫는 태그 </> 개수 확인
|
||||
orig_close = original.count('</>')
|
||||
trans_close = translation.count('</>')
|
||||
|
||||
if orig_close != trans_close:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.ERROR,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='태그 불일치',
|
||||
message=f'닫는 태그 </> 개수 불일치 (원문: {orig_close}, 번역: {trans_close})',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_newlines(self, msgctxt: str, original: str, translation: str, lang: str) -> List[ValidationIssue]:
|
||||
"""줄바꿈 문자 확인"""
|
||||
issues = []
|
||||
|
||||
# \r\n, \n, \r 개수 확인
|
||||
orig_crlf = original.count('\\r\\n')
|
||||
trans_crlf = translation.count('\\r\\n')
|
||||
|
||||
orig_lf = original.count('\\n') - orig_crlf # \\r\\n에 포함된 \\n 제외
|
||||
trans_lf = translation.count('\\n') - trans_crlf
|
||||
|
||||
orig_cr = original.count('\\r') - orig_crlf # \\r\\n에 포함된 \\r 제외
|
||||
trans_cr = translation.count('\\r') - trans_crlf
|
||||
|
||||
if orig_crlf != trans_crlf or orig_lf != trans_lf or orig_cr != trans_cr:
|
||||
issues.append(ValidationIssue(
|
||||
level=ValidationLevel.WARNING,
|
||||
msgctxt=msgctxt,
|
||||
lang=lang,
|
||||
category='줄바꿈 불일치',
|
||||
message=f'줄바꿈 문자 개수 불일치 (원문: \\r\\n={orig_crlf}, \\n={orig_lf}, \\r={orig_cr} / '
|
||||
f'번역: \\r\\n={trans_crlf}, \\n={trans_lf}, \\r={trans_cr})',
|
||||
original=original,
|
||||
translation=translation
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def validate_batch(self, entries: List[Dict]) -> Tuple[List[ValidationIssue], Dict[str, int]]:
|
||||
"""
|
||||
여러 항목 일괄 검증
|
||||
|
||||
Args:
|
||||
entries: [{'msgctxt': ..., 'msgid': ..., 'lang': ..., 'msgstr': ...}, ...]
|
||||
|
||||
Returns:
|
||||
(issues, stats)
|
||||
"""
|
||||
all_issues = []
|
||||
|
||||
for entry in entries:
|
||||
msgctxt = entry.get('msgctxt', '')
|
||||
original = entry.get('msgid', '')
|
||||
translation = entry.get('msgstr', '')
|
||||
lang = entry.get('lang', '')
|
||||
|
||||
issues = self.validate_entry(msgctxt, original, translation, lang)
|
||||
all_issues.extend(issues)
|
||||
|
||||
# 통계 생성
|
||||
stats = {
|
||||
'total': len(entries),
|
||||
'errors': sum(1 for issue in all_issues if issue.level == ValidationLevel.ERROR),
|
||||
'warnings': sum(1 for issue in all_issues if issue.level == ValidationLevel.WARNING),
|
||||
'info': sum(1 for issue in all_issues if issue.level == ValidationLevel.INFO),
|
||||
'passed': len(entries) - len(set(issue.msgctxt for issue in all_issues if issue.level == ValidationLevel.ERROR))
|
||||
}
|
||||
|
||||
return all_issues, stats
|
||||
|
||||
def print_validation_report(self, issues: List[ValidationIssue], stats: Dict[str, int], logger):
|
||||
"""검증 리포트 출력"""
|
||||
logger.section('🔍 번역 검증 결과')
|
||||
|
||||
if not issues:
|
||||
logger.success('✅ 모든 검증 통과!')
|
||||
logger.stats(**stats)
|
||||
return
|
||||
|
||||
# 레벨별로 그룹화
|
||||
errors = [i for i in issues if i.level == ValidationLevel.ERROR]
|
||||
warnings = [i for i in issues if i.level == ValidationLevel.WARNING]
|
||||
infos = [i for i in issues if i.level == ValidationLevel.INFO]
|
||||
|
||||
# 오류 출력
|
||||
if errors:
|
||||
logger.error(f'\n❌ 오류 ({len(errors)}건):')
|
||||
for issue in errors[:10]: # 최대 10개만 출력
|
||||
logger.error(f'\n{issue}')
|
||||
if len(errors) > 10:
|
||||
logger.error(f'\n... 외 {len(errors) - 10}건 더 있음')
|
||||
|
||||
# 경고 출력
|
||||
if warnings:
|
||||
logger.warning(f'\n⚠️ 경고 ({len(warnings)}건):')
|
||||
for issue in warnings[:10]: # 최대 10개만 출력
|
||||
logger.warning(f'\n{issue}')
|
||||
if len(warnings) > 10:
|
||||
logger.warning(f'\n... 외 {len(warnings) - 10}건 더 있음')
|
||||
|
||||
# 정보 출력
|
||||
if infos:
|
||||
logger.info(f'\nℹ️ 정보 ({len(infos)}건):')
|
||||
for issue in infos[:5]: # 최대 5개만 출력
|
||||
logger.info(f'\n{issue}')
|
||||
if len(infos) > 5:
|
||||
logger.info(f'\n... 외 {len(infos) - 5}건 더 있음')
|
||||
|
||||
# 통계 출력
|
||||
logger.separator()
|
||||
logger.stats(**stats)
|
||||
|
||||
if errors:
|
||||
logger.error(f'\n검증 실패: {stats["errors"]}개의 오류가 발견되었습니다.')
|
||||
else:
|
||||
logger.success(f'\n검증 완료: {stats["passed"]}건 통과 (경고 {stats["warnings"]}건)')
|
||||
Reference in New Issue
Block a user