perf & refactor: 重构 tools/AutoLocalization (#6995)

其中包含部分更优化和跟规范的算法或结构

---------

Co-authored-by: David <56174894+Constrat@users.noreply.github.com>
This commit is contained in:
quyansiyuanwang
2023-10-21 22:17:34 +08:00
committed by GitHub
parent 776f27ccc5
commit 490cfbf033
6 changed files with 474 additions and 281 deletions

View File

@@ -1,4 +1,4 @@
from src.auto_localization.cli import main
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,21 +1,40 @@
import argparse
import logging
import os
from shutil import copy
from pathlib import Path
from dotenv import load_dotenv
from .git import get_latest_file_content
from .xaml_load import XamlParser
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug(os.path.abspath('.env'))
load_dotenv(dotenv_path='.env')
if os.path.exists('../.env'):
load_dotenv(dotenv_path='../.env')
if os.path.exists('../../.env'):
load_dotenv(dotenv_path='../../.env')
root_path = os.getenv("LOCALIZATION_PATH")
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.debug(os.path.abspath(".env"))
solution_dir = Path.cwd()
for i in range(10):
cases = (
(solution_dir / "docs").exists(),
solution_dir / ".env" in solution_dir.iterdir(),
)
match cases:
case (_, True): # find .env by find itself
load_dotenv(dotenv_path=solution_dir / ".env")
break
case (True, _): # find .env by finding resource dir
env_path = solution_dir / "tools/AutoLocalization/.env"
if env_path.exists():
load_dotenv(dotenv_path=env_path)
break
case _: # if not found go up one level
solution_dir = solution_dir.parent
else:
logging.error("未找到.env文件")
exit(1)
root_path = solution_dir / os.getenv("LOCALIZATION_PATH")
assert root_path, "LOCALIZATION_PATH is not set"
zh_cn_path = os.path.join(root_path, "zh-cn.xaml")
en_us_path = os.path.join(root_path, "en-us.xaml")
@@ -26,11 +45,13 @@ ko_kr_path = os.path.join(root_path, "ko-kr.xaml")
def initiate(args):
logging.info("initiate project")
copy('./copy.env', '.env')
with open('./copy.env', 'r') as f:
copy("./copy.env", ".env")
with open("./copy.env", "r") as f:
content = f.read()
api_key = input("please input openai api key:")
content = content.replace('OPENAI_API_KEY=YOUR_API_KEY\n', f'OPENAI_API_KEY={api_key}\n')
content = content.replace(
"OPENAI_API_KEY=YOUR_API_KEY\n", f"OPENAI_API_KEY={api_key}\n"
)
with open(".env", "w") as f:
f.write(content)
@@ -86,7 +107,12 @@ def translate_compare(test):
def generate_by_language(test, force, path, language):
zh_cn_parser = XamlParser(zh_cn_path)
if force:
assert language in ["en-us", "zh-tw", "ja-jp", "ko-kr"], f"language {language} is not supported"
assert language in [
"en-us",
"zh-tw",
"ja-jp",
"ko-kr",
], f"language {language} is not supported"
language_path = os.path.join(path, f"{language}.xaml")
zh_cn_parser.translate_force(language_path, skip_translate=test)
return
@@ -106,44 +132,61 @@ def generate_by_language(test, force, path, language):
def translate_update(test):
zh_cn_new_parser = XamlParser(zh_cn_path)
zh_cn_old_content = get_latest_file_content(file_path=zh_cn_path, encoding=zh_cn_new_parser.encoding)
zh_cn_old_parser = XamlParser(parse_type=1,
xaml_string=zh_cn_old_content,
language=zh_cn_new_parser.language,
encoding=zh_cn_new_parser.encoding)
zh_cn_old_content = get_latest_file_content(
file_path=zh_cn_path, encoding=zh_cn_new_parser.encoding
)
zh_cn_old_parser = XamlParser(
parse_type=1,
xaml_string=zh_cn_old_content,
language=zh_cn_new_parser.language,
encoding=zh_cn_new_parser.encoding,
)
en_us_new_parser = XamlParser(en_us_path)
en_us_old_content = get_latest_file_content(file_path=en_us_path, encoding=en_us_new_parser.encoding)
en_us_old_parser = XamlParser(parse_type=1,
xaml_string=en_us_old_content,
language=en_us_new_parser.language,
encoding=en_us_new_parser.encoding)
en_us_old_content = get_latest_file_content(
file_path=en_us_path, encoding=en_us_new_parser.encoding
)
en_us_old_parser = XamlParser(
parse_type=1,
xaml_string=en_us_old_content,
language=en_us_new_parser.language,
encoding=en_us_new_parser.encoding,
)
zh_tw_parser = XamlParser(zh_tw_path)
ja_jp_parser = XamlParser(ja_jp_path)
ko_kr_parser = XamlParser(ko_kr_path)
zh_tw_parser.update_translate(zh_cn_old_parser, zh_cn_new_parser, skip_translate=test)
zh_tw_parser.update_translate(
zh_cn_old_parser, zh_cn_new_parser, skip_translate=test
)
# TODO: 英文更新翻译逻辑?
en_us_new_parser.translate_compare(zh_cn_new_parser, skip_translate=test)
en_us_new_parser = XamlParser(en_us_path)
ja_jp_parser.update_translate(en_us_old_parser, en_us_new_parser, skip_translate=test)
ko_kr_parser.update_translate(en_us_old_parser, en_us_new_parser, skip_translate=test)
ja_jp_parser.update_translate(
en_us_old_parser, en_us_new_parser, skip_translate=test
)
ko_kr_parser.update_translate(
en_us_old_parser, en_us_new_parser, skip_translate=test
)
logging.info("update done")
def update_by_language(test, path, language):
match language:
case s if s in ["en-us", 'zh-tw']:
case s if s in ["en-us", "zh-tw"]:
compare_file_path = zh_cn_path
case s if s in ["ja-jp", "ko-kr"]:
compare_file_path = en_us_path
case _:
raise ValueError(f"language {language} is not supported")
compare_new_parser = XamlParser(compare_file_path)
compare_old_content = get_latest_file_content(file_path=compare_file_path,
encoding=compare_new_parser.encoding)
compare_old_parser = XamlParser(parse_type=1,
xaml_string=compare_old_content,
language=compare_new_parser.language,
encoding=compare_new_parser.encoding)
compare_old_content = get_latest_file_content(
file_path=compare_file_path, encoding=compare_new_parser.encoding
)
compare_old_parser = XamlParser(
parse_type=1,
xaml_string=compare_old_content,
language=compare_new_parser.language,
encoding=compare_new_parser.encoding,
)
file_path = os.path.join(path, f"{language}.xaml")
parser = XamlParser(file_path)
parser.update_translate(compare_old_parser, compare_new_parser, skip_translate=test)
@@ -152,31 +195,40 @@ def update_by_language(test, path, language):
def cli_ui(test=None):
parser = argparse.ArgumentParser(description="一个用于自动翻译本地化目录下不同语言文档的命令行工具。")
subparsers = parser.add_subparsers()
parser_init = subparsers.add_parser('init', help='初始化工具')
parser_init = subparsers.add_parser("init", help="初始化工具")
parser_init.set_defaults(func=initiate)
parser_create = subparsers.add_parser('create', help='初始化其他语言的文档,可选参数:-f --force, -t --test')
parser_create = subparsers.add_parser(
"create", help="初始化其他语言的文档,可选参数:-f --force, -t --test"
)
parser_create.set_defaults(func=create)
parser_create.add_argument("-f", "--force", action="store_true", help="强制覆盖已有的部分")
parser_create.add_argument("-t", "--test", action="store_true", help="测试更新情况跳过chatgpt")
parser_create.add_argument("-l", "--language", help="指定语言可选参数en-us, zh-tw, ja-jp, ko-kr")
parser_create.add_argument(
"-t", "--test", action="store_true", help="测试更新情况跳过chatgpt"
)
parser_create.add_argument(
"-l", "--language", help="指定语言可选参数en-us, zh-tw, ja-jp, ko-kr"
)
parser_update = subparsers.add_parser('update', help='更新本地化翻译,可选参数:-t --test')
parser_update = subparsers.add_parser("update", help="更新本地化翻译,可选参数:-t --test")
parser_update.set_defaults(func=update)
parser_update.add_argument("-t", "--test", action="store_true", help="测试更新情况跳过chatgpt")
parser_update.add_argument("-l", "--language", help="指定语言可选参数en-us, zh-tw, ja-jp, ko-kr")
parser_update.add_argument(
"-t", "--test", action="store_true", help="测试更新情况跳过chatgpt"
)
parser_update.add_argument(
"-l", "--language", help="指定语言可选参数en-us, zh-tw, ja-jp, ko-kr"
)
if test:
args = parser.parse_args(test)
args.func(args)
else:
args = parser.parse_args()
args.func(args)
args.func(args)
def main():
cli_ui()
if __name__ == '__main__':
if __name__ == "__main__":
cli_ui(["init"])
print()

View File

@@ -1,26 +1,29 @@
import os.path
import subprocess
def get_latest_file_content(file_path='./cli.py', encoding='utf-8', tag_name=""):
def get_latest_file_content(file_path="./cli.py", encoding="utf-8", tag_name=""):
dirname = os.path.dirname(file_path)
basename = os.path.basename(file_path)
# 获取最近一次修改该文件的commit的ID
heading = 'HEAD'
heading = "HEAD"
if tag_name:
result = subprocess.run(['git', 'rev-list', '-n', '1', 'tag-autolocalization', '--', basename],
cwd=dirname,
capture_output=True,
text=True)
commit_id = result.stdout.replace('"', '').replace('\'', '').strip()
result = subprocess.run(
["git", "rev-list", "-n", "1", "tag-autolocalization", "--", basename],
cwd=dirname,
capture_output=True,
text=True,
)
commit_id = result.stdout.replace('"', "").replace("'", "").strip()
heading = commit_id if commit_id else heading
result = subprocess.run(['git', 'show', f'{heading}:./{basename}'],
cwd=dirname,
capture_output=True,
text=True,
encoding=encoding)
result = subprocess.run(
["git", "show", f"{heading}:./{basename}"],
cwd=dirname,
capture_output=True,
text=True,
encoding=encoding,
)
return result.stdout
if __name__ == '__main__':
print(get_latest_file_content('cli.py'))
if __name__ == "__main__":
print(get_latest_file_content("cli.py"))

View File

@@ -3,36 +3,55 @@ import logging
import os
import re
import time
from json import JSONDecodeError
import openai
from pathlib import Path
from json import JSONDecodeError
from dotenv import load_dotenv
from openai.error import RateLimitError, AuthenticationError
from opencc import OpenCC
logging.basicConfig(level=logging.INFO, format='MODULE:%(module)s - %(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.INFO,
format="MODULE:%(module)s - %(asctime)s - %(levelname)s - %(message)s",
)
class ChatTranslator:
def __init__(self, language: str = "english", base_language: str = "chinese"):
if os.path.exists('.env'):
load_dotenv(dotenv_path='.env')
elif os.path.exists('../.env'):
load_dotenv(dotenv_path='../.env')
elif os.path.exists('../../.env'):
load_dotenv(dotenv_path='../../.env')
self.base_language = base_language
self.language = language
solution_dir = Path.cwd()
for i in range(10):
cases = (
(solution_dir / "docs").exists(),
solution_dir / ".env" in solution_dir.iterdir(),
)
match cases:
case (_, True): # find .env by find itself
load_dotenv(dotenv_path=solution_dir / ".env")
break
case (True, _): # find .env by finding docs dir
env_path = solution_dir / "tools/AutoLocalization/.env"
if env_path.exists():
load_dotenv(dotenv_path=env_path)
break
case _: # if not found go up one level
solution_dir = solution_dir.parent
else:
logging.error("未找到.env文件")
exit(1)
self._api_key = os.environ.get('OPENAI_API_KEY')
self._api_key = os.environ.get("OPENAI_API_KEY")
assert self._api_key, "OPENAI_API_KEY is not set"
openai.api_key = self._api_key
self._model = os.environ.get('OPENAI_MODEL')
self._model = os.environ.get("OPENAI_MODEL")
if not self._model:
self._model = "gpt-3.5-turbo"
self._temperature = float(os.environ.get('OPENAI_TEMPERATURE'))
self._temperature = float(os.environ.get("OPENAI_TEMPERATURE"))
self._language = language
self._base_language = base_language
self._rules = """
@@ -59,115 +78,147 @@ class ChatTranslator:
@staticmethod
def generate_instruction(self, target_language, base_language="Chinese"):
return fr"""
return (
rf"""
1. Role
- you are a translator,don't ask anything, just translate everything i give from {base_language} to {target_language}.
""" + self._rules + """
"""
+ self._rules
+ """
- Don't forget the given rules. Now here comes the sentence or words you need to translate, please start to translate:"""
)
def translate(self, sentence: str = None, target_language: str = None, base_language: str = None, model=None,
temperature: float = None):
def translate(
self,
sentence: str = None,
target_language: str = None,
base_language: str = None,
model=None,
temperature: float = None,
):
# TODO 添加对话长度限制 添加代理
msg = ""
if None not in (target_language, base_language):
self.set_language(target_language, base_language)
if (
self._language == "Chinese (Traditional)"
and self._base_language == "Chinese (Simplified)"
):
return OpenCC("s2tw").convert(sentence) # 初始化转换器s2t表示从简体转繁体
if sentence is None:
sentence = self._test_sentence
if target_language is not None or base_language is not None:
self.set_language(target_language, base_language)
if model is None:
model = self._model
if temperature is None:
temperature = self._temperature
if self._language == "Chinese (Traditional)" and self._base_language == "Chinese (Simplified)":
# 初始化转换器s2t表示从简体转繁体
cc = OpenCC('s2tw')
return cc.convert(sentence)
new_sentence = sentence.replace(r'\n', r' \n ').replace('\n', ' \n ')
new_sentence = new_sentence.replace('&#x0a;', ' &#x0a; ')
msg = ""
new_sentence = sentence.replace(r"\n", r" \n ").replace("\n", " \n ")
new_sentence = new_sentence.replace("&#x0a;", " &#x0a; ")
message = [
{"role": "system", "content": self._instruction},
{"role": "user", "content": '1'},
{"role": "user", "content": "1"},
{"role": "assistant", "content": '{"message":200,"content":"1"}'},
{"role": "user", "content": 'abd,l'},
{"role": "user", "content": "abd,l"},
{"role": "assistant", "content": '{"message":404,"content":"abd,l"}'},
{"role": "user", "content": '1122 \\n 3344&#x0a;55666&lt;'},
{"role": "assistant", "content": '{"message":200,"content":"1122 \\n 3344&#x0a;55666&lt;"}'},
{"role": "user", "content": "1122 \\n 3344&#x0a;55666&lt;"},
{
"role": "assistant",
"content": '{"message":200,"content":"1122 \\n 3344&#x0a;55666&lt;"}',
},
{"role": "user", "content": new_sentence},
]
def append_new_msg(i_: int, msg_) -> bool:
if i_ < 9:
message.append({"role": "assistant", "content": msg_})
message.append({"role": "user", "content": new_sentence})
return True
return False
def log_err(
e_=None, info: str = "", append_switch: bool = True
) -> None: # return it runResult equal to return None
if append_switch:
info = f"{type(e_).__name__}: {e_} msg:{msg}" + info
logging.error(info)
for i in range(10):
try:
completion = openai.ChatCompletion.create(
model=model,
temperature=temperature,
messages=message
model=model, temperature=temperature, messages=message
)
msg = completion['choices'][0]['message']['content'].strip()
msg = '{' + msg[2:] if msg.startswith("{{") else msg
msg = msg[:-2] + '}' if msg.endswith("}}") else msg
msg = msg.replace('\n', '\\n') if '\n' in msg else msg
msg = completion["choices"][0]["message"]["content"].strip()
msg = msg[1:] if msg.startswith("{{") else msg
msg = msg[:-1] if msg.endswith("}}") else msg
msg = msg.replace("\n", "\\n") if "\n" in msg else msg
msg_json = json.loads(msg)
time.sleep(0.1)
except RateLimitError as _:
except RateLimitError:
time.sleep(2)
continue
except JSONDecodeError as _:
if i < 9:
message.append({"role": "assistant", "content": msg})
message.append({"role": "user", "content": new_sentence})
except JSONDecodeError as e:
if append_new_msg(i, msg):
continue
pt = re.compile(r"{[^{].*?:.*?,.*?:[^}]*}")
if pt.search(msg):
msg = pt.search(msg).group()
search_msg = re.search(r"{[^{].*?:.*?,.*?:[^}]*}", msg)
if search_msg is not None:
msg = search_msg.group()
try:
msg_json = json.loads(msg)
except Exception as _:
logging.error(f"{type(_).__name__}: {_} msg:{msg}")
return None
except Exception as e:
return log_err(e)
else:
logging.error(f"{type(_).__name__}: {_} msg:{msg}")
return None
except AuthenticationError as _:
logging.error(f"{type(_).__name__}: {_} msg:{msg},maybe key not set correctly")
return None
except Exception as _:
if i < 9:
message.append({"role": "assistant", "content": msg})
message.append({"role": "user", "content": new_sentence})
continue
logging.error(f"{type(_).__name__}: {_} msg:{msg}")
return None
match msg_json['message']:
case 200:
# logging.info(f"translate success")
content = msg_json['content']
# .replace(r'$\\n$', '\\n').replace(r'$\n$', '\\n').replace('$\n$', '\\n')
return content
case 404:
if i < 9:
message.append({"role": "assistant", "content": msg})
message.append({"role": "user", "content": new_sentence})
continue
logging.error(f"translate error:{new_sentence}| {msg_json['content']}")
return None
case _:
if i < 9:
message.append({"role": "assistant", "content": msg})
message.append({"role": "user", "content": new_sentence})
continue
logging.error(f"translate error: {msg_json}")
return None
return log_err(e)
def set_language(self, target_language, base_language):
self._language = target_language if target_language is not None else self._language
self._base_language = base_language if base_language is not None else self._base_language
self._instruction = self.generate_instruction(self, self._language, self._base_language)
except AuthenticationError as e:
return log_err(e, info=",maybe key not set correctly")
except Exception as e:
if append_new_msg(i, msg):
continue
return log_err(e)
if msg_json["message"] == 200: # case 200
return msg_json["content"]
if append_new_msg(i, msg):
continue
match msg_json["message"]:
case 404:
log_err(
info=f"translate error:{new_sentence}| {msg_json['content']}",
append_switch=False,
)
case _:
log_err(info=f"translate error: {msg_json}", append_switch=False)
def set_language(self, target_language=None, base_language=None):
if target_language is not None:
self._language = target_language
if base_language is not None:
self._base_language = base_language
self._instruction = self.generate_instruction(
self, self._language, self._base_language
)
def add_rules(self, rules: str):
self._rules += """
- """ + rules.strip()
self._instruction = self.generate_instruction(self, self._language, self._base_language)
self._rules += (
"""
- """
+ rules.strip()
)
self._instruction = self.generate_instruction(
self, self._language, self._base_language
)
# if __name__ == '__main__':
# a = ChatTranslator()
# a.translate()
# print()
if __name__ == "__main__":
# a = ChatTranslator()
# a.translate()
pass

View File

@@ -1,42 +1,43 @@
import logging
import os
import re
from copy import deepcopy
from copy import deepcopy
from cchardet import detect
from lxml import etree
from xmldiff import main
from xmldiff.actions import UpdateTextIn, InsertComment, UpdateTextAfter, DeleteNode
from .translate import ChatTranslator
SPACE = '{http://www.w3.org/XML/1998/namespace}space'
SPACE = "{http://www.w3.org/XML/1998/namespace}space"
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
def judge_encoding(file):
with open(file, 'rb') as _:
with open(file, "rb") as _:
content = _.read()
result = detect(content)
return result['encoding']
return result["encoding"]
def parse_lang_str(doc_path):
lang_str = os.path.basename(doc_path)
match lang_str:
case s if 'zh-cn' in s:
case s if "zh-cn" in s:
return "Chinese (Simplified)"
case s if 'zh-tw' in s:
case s if "zh-tw" in s:
return "Chinese (Traditional)"
case s if 'en-us' in s:
case s if "en-us" in s:
return "English"
case s if 'ja-jp' in s:
case s if "ja-jp" in s:
return "Japanese"
case s if 'ko-kr' in s:
case s if "ko-kr" in s:
return "Korean"
case _:
raise ValueError('wrong language input')
raise ValueError("wrong language input")
class XamlParser:
@@ -45,7 +46,9 @@ class XamlParser:
"""
def __init__(self, file=None, parse_type=0, xaml_string=None, language=None, encoding='utf-8'):
def __init__(
self, file=None, parse_type=0, xaml_string=None, language=None, encoding="utf-8"
):
"""
初始化XamlParser类
:param file: 从文件解析时,传入文件路径
@@ -57,34 +60,59 @@ class XamlParser:
self.__test = False
self.__counter = 0
if parse_type == 0:
self._file_path, self.__encoding, self.__language, xaml_string = self.__from_file(file)
(
self._file_path,
self.__encoding,
self.__language,
xaml_string,
) = self.__from_file(file)
elif parse_type == 1:
assert language is not None, 'language must be specified when parse_type is 1'
assert (
language is not None
), "language must be specified when parse_type is 1"
self._file_path, self.__encoding, self.__language = file, encoding, language
else:
raise ValueError('parse_type must be 0 or 1')
xaml_string = xaml_string.replace('&#x0a;', '\\n')
raise ValueError("parse_type must be 0 or 1")
xaml_string = xaml_string.replace("&#x0a;", "\\n")
self.__root = etree.fromstring(xaml_string)
# 获取命名空间
self.__nsmap = self.__root.nsmap
self.__default_namespace = self.__nsmap.get(None)
self.__x_ns = self.__nsmap.get('x')
self.__x_uid_ns, self.__x_key_ns = f'{{{self.__x_ns}}}Uid', f'{{{self.__x_ns}}}Key'
self.__merged_node = self.__root.find('./ResourceDictionary.MergedDictionaries', namespaces=self.__nsmap)
self.__x_ns = self.__nsmap.get("x")
self.__x_uid_ns, self.__x_key_ns = (
f"{{{self.__x_ns}}}Uid",
f"{{{self.__x_ns}}}Key",
)
self.__merged_node = self.__root.find(
"./ResourceDictionary.MergedDictionaries", namespaces=self.__nsmap
)
self.__cp_root = self.copy_node(self.__root, cp_text=True)
if self.__merged_node is not None:
self.__cp_merged_node = self.copy_node(self.__merged_node, cp_text=True)
self.__cp_root.append(self.__cp_merged_node)
else:
self.__cp_merged_node = None
self.__gen_cp_tree_by_traverse(element=self.__root, current_cp_node=self.__cp_root)
self.__gen_cp_tree_by_traverse(
element=self.__root, current_cp_node=self.__cp_root
)
def __getattr__(self, prop):
properties_map = {
"encoding": self.__encoding,
"cp_tree": self.__cp_root,
"tree": self.__root,
"x_uid_ns": self.__x_uid_ns,
"x_key_ns": self.__x_key_ns,
"language": self.__language,
}
return properties_map.get(prop, None)
@staticmethod
def __from_file(file):
assert os.path.exists(file), f'{file}file not exists'
assert os.path.exists(file), f"{file}file not exists"
encoding = judge_encoding(file)
language = parse_lang_str(file)
with open(file, 'r', encoding=encoding) as f:
with open(file, "r", encoding=encoding) as f:
content = f.read()
return file, encoding, language, content
@@ -98,7 +126,11 @@ class XamlParser:
children = list(parent)
index = children.index(child)
if index == 0:
if self.__merged_node is not None and parent.tag == f'{{{self.__default_namespace}}}ResourceDictionary':
if (
self.__merged_node is not None
and parent.tag
== f"{{{self.__default_namespace}}}ResourceDictionary"
):
continue
# 注释节点为第一个节点
cp_comment = self.copy_node(child, cp_text=False)
@@ -107,17 +139,23 @@ class XamlParser:
cp_comment = self.copy_node(child, cp_text=False)
prev_node = children[index - 1]
prev_cp_node = current_cp_node[index - 1]
assert prev_node.tag == prev_cp_node.tag and prev_node.attrib == prev_cp_node.attrib, "前节点类型不一致"
assert (
prev_node.tag == prev_cp_node.tag
and prev_node.attrib == prev_cp_node.attrib
), "前节点类型不一致"
current_cp_node.append(cp_comment)
continue
cp_node = self.copy_node(child)
if child.tag == f'{{{self.__default_namespace}}}ResourceDictionary.MergedDictionaries':
if (
child.tag
== f"{{{self.__default_namespace}}}ResourceDictionary.MergedDictionaries"
):
# 次根 MergedDictionaries: 遍历后退出循环
current_cp_node = self.__cp_merged_node
self.__gen_cp_tree_by_traverse(child, current_cp_node=current_cp_node)
break
elif child.tag == f'{{{self.__default_namespace}}}ResourceDictionary':
elif child.tag == f"{{{self.__default_namespace}}}ResourceDictionary":
# resource dictionary uid组
cp_node.text = child.text
current_cp_node.append(cp_node)
@@ -166,7 +204,7 @@ class XamlParser:
xaml_data = etree.tostring(tree, pretty_print=True, encoding=self.__encoding)
# xaml_data = html.unescape(xaml_data.decode(self.__encoding))
xaml_data = xaml_data.decode(self.__encoding)
with open(file_path, 'w', encoding=self.__encoding) as _:
with open(file_path, "w", encoding=self.__encoding) as _:
_.write(xaml_data)
return True
@@ -178,28 +216,35 @@ class XamlParser:
"""
this_tree = self.cp_tree
compare_tree = compare_parser.cp_tree
assert self.x_key_ns == compare_parser.x_key_ns, f"{self.language}{compare_parser.language}x:Key命名空间不一致"
assert self.x_uid_ns == compare_parser.x_uid_ns, f"{self.language}{compare_parser.language}x:Uid命名空间不一致"
pt = re.compile(r'/\*/\*/\*\[\d*]/comment\(\)\[1]')
assert (
self.x_key_ns == compare_parser.x_key_ns
), f"{self.language}{compare_parser.language}x:Key命名空间不一致"
assert (
self.x_uid_ns == compare_parser.x_uid_ns
), f"{self.language}{compare_parser.language}x:Uid命名空间不一致"
pt = re.compile(r"/\*/\*/\*\[\d*]/comment\(\)\[1]")
uniqueattrs = [self.x_key_ns, self.x_uid_ns]
ignored_attrs = [SPACE]
res = main.diff_trees(this_tree, compare_tree, diff_options={
'F': 0.1,
'ratio_mode': 'accurate',
'uniqueattrs': uniqueattrs,
'ignored_attrs': ignored_attrs, })
for i in res:
if type(i).__name__ == 'UpdateTextIn' and pt.search(i.node):
continue
res = main.diff_trees(
this_tree,
compare_tree,
diff_options={
"F": 0.1,
"ratio_mode": "accurate",
"uniqueattrs": uniqueattrs,
"ignored_attrs": ignored_attrs,
},
)
if any((isinstance(i, UpdateTextIn) and pt.search(i.node)) for i in res):
return False
return True
def xpath(self, xpath_str, only_one=True, accept_empty=False):
ns = self.nsmap
if None in ns:
del ns[None]
if "" in ns:
del ns[""]
del_arr = (None, "")
for remove_elem in del_arr:
if remove_elem in ns:
del ns[remove_elem]
search_result = self.tree.xpath(xpath_str, namespaces=ns)
if not len(search_result):
assert accept_empty, f"xpath: {xpath_str} 搜索结果为空"
@@ -235,14 +280,22 @@ class XamlParser:
"""
target_language = parse_lang_str(target_path)
output_tree = deepcopy(self.cp_tree)
chat = None if skip_translate else ChatTranslator(language=target_language, base_language=self.language)
self.counter(start=True,
test=skip_translate,
messages=f"start force translate {self.language} -> {target_language}")
chat = (
None
if skip_translate
else ChatTranslator(language=target_language, base_language=self.language)
)
self.counter(
start=True,
test=skip_translate,
messages=f"start force translate {self.language} -> {target_language}",
)
t = self.__merged_node if self.__merged_node is not None else self.__root
for i in t.findall('.//s:String[@x:Key]', namespaces=self.__nsmap):
for i in t.findall(".//s:String[@x:Key]", namespaces=self.__nsmap):
key = i.get(self.__x_key_ns)
node = output_tree.find(f'.//s:String[@x:Key="{key}"]', namespaces=self.__nsmap)
node = output_tree.find(
f'.//s:String[@x:Key="{key}"]', namespaces=self.__nsmap
)
node.text = i.text if skip_translate else chat.translate(i.text)
node.text = i.text if node.text is None else node.text
self.counter(messages=f"translate {i.text[0:20]}")
@@ -258,60 +311,94 @@ class XamlParser:
"""
target_cp_tree = self.cp_tree
base_cp_tree = compare_parser.cp_tree
assert self.nsmap == compare_parser.nsmap, f"{self.language}{compare_parser.language}命名空间不一致"
assert self.x_key_ns == compare_parser.x_key_ns, f"{self.language}{compare_parser.language}x:Key命名空间不一致"
assert self.x_uid_ns == compare_parser.x_uid_ns, f"{self.language}{compare_parser.language}x:Uid命名空间不一致"
assert (
self.nsmap == compare_parser.nsmap
), f"{self.language}{compare_parser.language}命名空间不一致"
assert (
self.x_key_ns == compare_parser.x_key_ns
), f"{self.language}{compare_parser.language}x:Key命名空间不一致"
assert (
self.x_uid_ns == compare_parser.x_uid_ns
), f"{self.language}{compare_parser.language}x:Uid命名空间不一致"
uniqueattrs = [self.x_key_ns, self.x_uid_ns]
ignored_attrs = [SPACE]
chat = None if skip_translate else ChatTranslator(language=self.language, base_language=compare_parser.language)
self.counter(start=True,
test=skip_translate,
messages=f"start compare translate {compare_parser.language} -> {self.language}")
res = main.diff_trees(target_cp_tree, base_cp_tree, diff_options={
'F': 0.1,
'ratio_mode': 'accurate',
'uniqueattrs': uniqueattrs,
'ignored_attrs': ignored_attrs, })
chat = (
None
if skip_translate
else ChatTranslator(
language=self.language, base_language=compare_parser.language
)
)
self.counter(
start=True,
test=skip_translate,
messages=f"start compare translate {compare_parser.language} -> {self.language}",
)
res = main.diff_trees(
target_cp_tree,
base_cp_tree,
diff_options={
"F": 0.1,
"ratio_mode": "accurate",
"uniqueattrs": uniqueattrs,
"ignored_attrs": ignored_attrs,
},
)
new_action = []
logging.info(f"all movements contains {len([i for i in res if type(i).__name__ == 'UpdateTextIn'])} steps")
logging.info(
f"all movements contains {len([i for i in res if isinstance(i, UpdateTextIn)])} steps"
)
for i in res:
if type(i).__name__ == 'MoveNode' and 'comment()' in i.node:
if type(i).__name__ == "MoveNode" and "comment()" in i.node:
try:
str_node = next(compare_parser.xpath(i.target))[i.position]
parts = i.node.split('/')
parts = i.node.split("/")
# 提取父节点的路径
parent_path = '/'.join(parts[:-1])
node_position = "{}/comment()[{}]".format(i.target,
len(str_node.xpath('preceding-sibling::comment()')) + 1)
parent_path = "/".join(parts[:-1])
node_position = "{}/comment()[{}]".format(
i.target,
len(str_node.xpath("preceding-sibling::comment()")) + 1,
)
if parent_path == compare_parser.getpath(str_node.getparent()):
# 如果父节点路径相同,说明
new_action.append(i)
new_action.append(UpdateTextIn(node_position, str_node.text))
continue
new_action.append(InsertComment(i.target, i.position, str_node.text))
new_action.append(
InsertComment(i.target, i.position, str_node.text)
)
new_action.append(UpdateTextAfter(node_position, str_node.tail))
new_action.append(DeleteNode(i.node))
except Exception as _:
logging.warning(f"{type(_)}: {_}")
new_action.append(i)
elif type(i).__name__ == 'UpdateTextIn':
if 'comment()' in i.node:
elif isinstance(i, UpdateTextIn):
if "comment()" in i.node:
continue
str_node = next(compare_parser.xpath(i.node))
if str_node.text.strip():
text = str_node.text if skip_translate else chat.translate(str_node.text)
text = (
str_node.text
if skip_translate
else chat.translate(str_node.text)
)
text = str_node.text if text is None else text
else:
text = str_node.text
self.counter(messages=f"translate {str_node.text[0:20]}")
new_action.append(UpdateTextIn(i.node, text))
elif type(i).__name__ == 'InsertComment':
elif isinstance(i, InsertComment):
str_node = next(compare_parser.xpath(i.target))[i.position]
if str_node.tag == etree.Comment:
node_position = "{}/comment()[{}]".format(i.target,
len(str_node.xpath('preceding-sibling::comment()')) + 1)
new_action.append(InsertComment(i.target, i.position, str_node.text))
node_position = "{}/comment()[{}]".format(
i.target,
len(str_node.xpath("preceding-sibling::comment()")) + 1,
)
new_action.append(
InsertComment(i.target, i.position, str_node.text)
)
new_action.append(UpdateTextAfter(node_position, str_node.tail))
else:
new_action.append(i)
@@ -322,7 +409,9 @@ class XamlParser:
self.write_xaml(final_tree, self._file_path)
return final_tree
def update_translate(self, compare_old_parser, compare_new_parser, skip_translate=False):
def update_translate(
self, compare_old_parser, compare_new_parser, skip_translate=False
):
"""
将比较新旧两个文件的更新并将更新同步到本文件若本文件与旧文件结构不同则会与旧文件调用translate_compare
:param compare_old_parser: 旧文件的XamlParser
@@ -332,75 +421,70 @@ class XamlParser:
"""
compare_old_tree = compare_old_parser.merged_root_tree
if not self.compare_structure(compare_old_parser):
target_tree = self.translate_compare(compare_old_parser, skip_translate=skip_translate)
target_tree = self.translate_compare(
compare_old_parser, skip_translate=skip_translate
)
else:
target_tree = self.cp_tree
compare_new_tree = compare_new_parser.merged_root_tree
assert compare_old_parser.nsmap == compare_new_parser.nsmap, \
f"old {compare_old_parser.language}和 new {compare_new_parser.language}命名空间不一致"
assert compare_old_parser.x_key_ns == compare_new_parser.x_key_ns, \
f"old {compare_old_parser.language}和 new {compare_new_parser.language}x:Key命名空间不一致"
assert compare_old_parser.x_uid_ns == compare_new_parser.x_uid_ns, \
f"old {compare_old_parser.language}和 new {compare_new_parser.language}x:Uid命名空间不一致"
namespace_ununit_string = "old %s和 new %s{}命名空间不一致" % (
compare_old_parser.language,
compare_new_parser.language,
)
for prop, err in {"nsmap": "", "x_key_ns": "x:Key", "x_uid_ns": "x:Uid"}:
assert getattr(compare_old_parser, prop) == getattr(
compare_new_parser, prop
), namespace_ununit_string.format(err)
chat = None if skip_translate else ChatTranslator(language=self.language,
base_language=compare_new_parser.language)
self.counter(start=True,
test=skip_translate,
messages=f"start compare translate {compare_new_parser.language} -> {self.language}")
chat = (
None
if skip_translate
else ChatTranslator(
language=self.language, base_language=compare_new_parser.language
)
)
self.counter(
start=True,
test=skip_translate,
messages=f"start compare translate {compare_new_parser.language} -> {self.language}",
)
uniqueattrs = [self.x_key_ns, self.x_uid_ns]
ignored_attrs = [SPACE]
res = main.diff_trees(compare_old_tree, compare_new_tree, diff_options={
'F': 0.1,
'ratio_mode': 'accurate',
'uniqueattrs': uniqueattrs,
'ignored_attrs': ignored_attrs
})
res = main.diff_trees(
compare_old_tree,
compare_new_tree,
diff_options={
"F": 0.1,
"ratio_mode": "accurate",
"uniqueattrs": uniqueattrs,
"ignored_attrs": ignored_attrs,
},
)
new_actions = []
logging.info(f"all movements contains {len([i for i in res if type(i).__name__ == 'UpdateTextIn'])} steps")
logging.info(
f"all movements contains {len([i for i in res if isinstance(i, UpdateTextIn)])} steps"
)
for i in res:
if type(i).__name__ == 'UpdateTextIn':
if i.node.endswith('comment()[1]') and i.text[0] == '$':
if isinstance(i, UpdateTextIn):
if i.node.endswith("comment()[1]") and i.text[0] == "$":
continue
elif 's:String' in i.node:
elif "s:String" in i.node:
text = i.text if skip_translate else chat.translate(i.text)
text = i.text if text is None else text
self.counter(messages=f"translate {i.text[0:20]}")
new_actions.append(UpdateTextIn(i.node, text))
else:
new_actions.append(i)
else:
new_actions.append(i)
continue
new_actions.append(i)
final_tree = main.patch_tree(new_actions, target_tree)
compare_new_parser.write_xaml()
self.write_xaml(final_tree)
return final_tree
@property
def encoding(self):
return self.__encoding
@property
def cp_tree(self):
return self.__cp_root
@property
def tree(self):
return self.__root
@property
def nsmap(self):
return deepcopy(self.__nsmap)
@property
def x_uid_ns(self):
return self.__x_uid_ns
@property
def x_key_ns(self):
return self.__x_key_ns
@property
def merged_root_tree(self):
root = self.copy_node(self.__root, cp_text=True)
@@ -410,16 +494,14 @@ class XamlParser:
root = deepcopy(self.__root)
return root
@property
def language(self):
return self.__language
@property
def tostring(self):
return etree.tostring(self.tree, encoding=self.__encoding, pretty_print=True).decode(self.__encoding)
return etree.tostring(
self.tree, encoding=self.__encoding, pretty_print=True
).decode(self.__encoding)
if __name__ == '__main__':
zh_parser = XamlParser(parse_type=0, file='../../example/zh-cn.xaml')
zh_new_parser = XamlParser(parse_type=0, file='../../example/zh-cn_new.xaml')
if __name__ == "__main__":
zh_parser = XamlParser(parse_type=0, file="../../example/zh-cn.xaml")
zh_new_parser = XamlParser(parse_type=0, file="../../example/zh-cn_new.xaml")
print()

View File

@@ -0,0 +1,5 @@
from _test_all import TestAll
from test_translate import TestChatTranslator
from test_xaml_load import TestXamlParser
__all__ = ['TestAll', 'TestChatTranslator', 'TestXamlParser']