import re
import os
import sys
import time
import asyncio
import chardet
import tempfile
import pathlib
from pprint import pprint
import polib
from googletrans import Translator
from javalang.parse import parse as javaparse
from javalang import tree as javatree
from pyjsparser import parse as jsparse
from xml.etree import ElementTree
TRANSLATE_RESULT_DIR = ".trans"
TRANSLATE_RECORD_FILE = os.path.join(TRANSLATE_RESULT_DIR, ".record")
PROXY = None #代理仅支持http/https
DELAY = 0.2 #翻译间隔,避免被禁
KOREAN_PATTERN = re.compile(r'[\uAC00-\uD7A3]+')
BACKUP_EXTENTION = ".tbak"
if not os.path.isdir(TRANSLATE_RESULT_DIR):
os.makedirs(TRANSLATE_RESULT_DIR)
async def translate_korean_in_normal_file(file_path):
"""快速翻译单个文件中的韩文(直接替换原文件)"""
# 读取文件
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except:
try:
with open(file_path, 'r', encoding='cp949') as f:
content = f.read()
except:
print("无法读取文件编码")
return
# 查找韩文
korean_matches = KOREAN_PATTERN.findall(content)
if not korean_matches:
return
print(f"发现 {len(korean_matches)} 处韩文内容")
# 翻译并替换
translator = Translator(proxy=PROXY)
translated_content = content
translation_cache = {}
for korean in set(korean_matches): # 去重
if korean in translation_cache:
chinese = translation_cache[korean]
else:
try:
result = await translator.translate(korean, src='ko', dest='zh-cn')
chinese = result.text
translation_cache[korean] = chinese
time.sleep(DELAY)
except Exception as e:
print(f"翻译失败 '{korean}': {e}")
chinese = korean
print(f"翻译: '{korean}' -> '{chinese}'")
translated_content = translated_content.replace(korean, chinese)
# 写回原文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(translated_content)
print(f"文件 {file_path} 翻译完成")
def translate_korean_in_java_file(file_path):
try:
source_content = open(file_path, 'r', encoding='utf-8').read()
tree = javaparse(source_content)
except:
print(f"解析java文件{file_path}失败")
return
output_file = str(pathlib.Path(TRANSLATE_RESULT_DIR, file_path)) + ".po"
os.makedirs(os.path.dirname(output_file), exist_ok=True)
po = polib.POFile()
for _, node in tree:
if not isinstance(node, javatree.Literal):
continue
original_value = node.value
if original_value.startswith('"') and original_value.endswith('"') and \
KOREAN_PATTERN.findall(original_value):
entry = polib.POEntry(
msgid = original_value[1:-1],
msgstr = "" #翻译结果
)
po.append(entry)
#print("需要翻译:", original_value)
#translated_value = self._translate_text(original_value)
#node.value = translated_value
if len(po) > 0:
po.save(output_file)
def translate_korean_in_xml_file(file_path):
try:
source_content = open(file_path, 'r', encoding='utf-8').read()
xmltree = ElementTree.fromstring(source_content)
except:
print(f"解析xml文件{file_path}失败")
return
output_file = str(pathlib.Path(TRANSLATE_RESULT_DIR, file_path)) + ".po"
os.makedirs(os.path.dirname(output_file), exist_ok=True)
po = polib.POFile()
for word in xmltree.findall(".//*[@value]"):
value = word.get('value')
if KOREAN_PATTERN.findall(value):
entry = polib.POEntry(
msgid = value,
msgstr = "" #翻译结果
)
po.append(entry)
#print("需要翻译:", value)
if len(po) > 0:
po.save(output_file)
def translate_korean_in_js_file(file_path):
try:
source_content = open(file_path, 'rb').read()
encoding = chardet.detect(source_content)['encoding'] or 'utf-8'
source_content = source_content.decode(encoding)
tree = jsparse(source_content)
except: #utf8变量会报错
print(f"解析js文件{file_path}失败")
#raise
return
#pprint(tree, open("JSparsed.txt", "w"))
#return
def search_variable_or_argument(search_tree):
if search_tree['type'] in ['Program', 'BlockStatement']:
for node in search_tree['body']:
yield from search_variable_or_argument(node)
if search_tree['type'] in ['FunctionDeclaration', 'CatchClause']:
yield from search_variable_or_argument(search_tree['body'])
if search_tree['type'] == 'ExpressionStatement':
yield from search_variable_or_argument(search_tree['expression'])
if search_tree['type'] == 'TryStatement':
for node in search_tree['handlers']:
yield from search_variable_or_argument(node)
if search_tree['type'] == 'CallExpression':
for node in search_tree['arguments']:
yield from search_variable_or_argument(node)
if search_tree['type'] == 'BinaryExpression':
yield from search_variable_or_argument(search_tree['left'])
yield from search_variable_or_argument(search_tree['right'])
if search_tree['type'] in ['Literal', 'VariableDeclaration']:
if 'value' in search_tree and isinstance(search_tree['value'], str) and \
KOREAN_PATTERN.findall(search_tree['value']):
yield search_tree['value']
output_file = str(pathlib.Path(TRANSLATE_RESULT_DIR, file_path)) + ".po"
os.makedirs(os.path.dirname(output_file), exist_ok=True)
po = polib.POFile()
for word in search_variable_or_argument(tree):
entry = polib.POEntry(
msgid = word,
msgstr = "" #翻译结果
)
po.append(entry)
#print("需要翻译:", word)
if len(po) > 0:
po.save(output_file)
def add_to_translated(record):
with open(TRANSLATE_RECORD_FILE, "a") as f:
f.write(f"{record}\n")
def load_translated():
records = []
if os.path.isfile(TRANSLATE_RECORD_FILE):
with open(TRANSLATE_RECORD_FILE, "r") as f:
while (line := f.readline()):
record = line.strip("\n")
if record:
records.append(record)
return records
def main():
translated_files = load_translated()
for dirpath, dirs, files in os.walk("."):
#跳过.开头的目录
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
fpath = os.path.join(dirpath, f)
if fpath not in translated_files:
add_to_translated(fpath)
ext = f.split(".")[-1]
if ext == "java":
translate_korean_in_java_file(fpath)
elif ext == "js":
translate_korean_in_js_file(fpath)
elif ext == "xml":
translate_korean_in_xml_file(fpath)
else:
continue
def _import(file_or_dir=""):
if not file_or_dir:
file_or_dir = TRANSLATE_RESULT_DIR
if os.path.isfile(file_or_dir):
relpath = pathlib.Path(os.path.relpath(file_or_dir))
if relpath.is_absolute():
return
if relpath.parts[0] == TRANSLATE_RESULT_DIR:
source_file = os.path.join(*pathlib.Path(file_or_dir).parts[1:])[:-3] #去掉.trans,.po
else:
source_file = file_or_dir
file_or_dir = str(pathlib.Path(TRANSLATE_RESULT_DIR, file_or_dir)) + ".po"
po = polib.pofile(file_or_dir)
if os.path.isfile(source_file):
content = open(source_file, 'rb').read()
source_encoding = chardet.detect(content)['encoding'] or 'utf-8'
content = content.decode(source_encoding)
for entry in po:
if entry.msgid and entry.msgstr:
content = content.replace(entry.msgid.replace("\t", "\\t").replace("\n", "\\n"),
entry.msgstr.replace("\t", "\\t").replace("\n", "\\n"))
tmpfile = tempfile.NamedTemporaryFile(mode="w", encoding=source_encoding, dir=TRANSLATE_RESULT_DIR,
delete=False, newline='' if os.name == 'nt' else None)
tmpfile.write(content)
tmpfile.close()
print('importing', source_file)
backup_path = source_file + BACKUP_EXTENTION
if not os.path.isfile(backup_path):
os.rename(source_file, source_file + BACKUP_EXTENTION)
os.replace(tmpfile.name, source_file)
return
if os.path.isdir(file_or_dir):
for dirpath, dirs, files in os.walk(file_or_dir):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
if f.split('.')[-1] not in ['po', 'js', 'java', 'xml']:
continue
_import(os.path.join(dirpath, f))
return
def revert(file_or_dir=""):
if not file_or_dir:
file_or_dir = "."
current_dir_realpath = os.path.realpath(os.getcwd())
if os.path.isfile(file_or_dir):
if not file_or_dir.endswith(BACKUP_EXTENTION):
return
file_realpath = os.path.realpath(file_or_dir)
if not file_realpath.startswith(current_dir_realpath): #只还原本项目内的文件
return
revert_path = file_realpath[:-len(BACKUP_EXTENTION)]
print("reverting", revert_path)
os.replace(file_realpath, revert_path)
return
if os.path.isdir(file_or_dir):
for dirpath, dirs, files in os.walk(file_or_dir):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
revert(os.path.join(dirpath, f))
return
if __name__ == "__main__":
sysArgs = sys.argv[1:]
if sysArgs:
if sysArgs[0] == '--import':
_import(sysArgs[1] if len(sysArgs) > 1 else "")
elif sysArgs[0] == '--revert':
revert(sysArgs[1] if len(sysArgs) > 1 else "")
else:
print(f"usage: python {os.path.basename(__file__)} [--import | --revert]")
else:
main()