import queue
import threading
import xml.etree.ElementTree as ET
import os
from concurrent.futures.thread import ThreadPoolExecutor
import re
from pathlib import Path
from collections import defaultdict
import copy
from pathlib import Path
import openpyxl
from openpyxl import Workbook
import tkinter as tk
from tkinter import ttk
suffixes_android = ['strings.xml', 'strings_v6.xml', 'strings_v2.xml']
suffixes_ios = ['Localizable.strings', 'LocalizableAdded.strings']
title_path = set()
all_differ_key_value = []
queues_name_gloable = ['remediate_list']
queues_name = {name: queue.Queue() for name in queues_name_gloable}
# 创建布尔变量及选框
language_vars = {}
# 公共资源
ios_self_common = []
# 定制资源
ios_self_tai = []
lock = threading.Lock()
class xmlObj:
def __init__(self, key, value, old_value, old_key, path, ios_key, ios_value, ios_path, languages):
self.key = key
self.xml_path = path
self.value = value
self.languages = languages
self.ios_key = ios_key
self.ios_value = ios_value
self.ios_path = ios_path
self.old_value = old_value
self.old_key = old_key
def __eq__(self, other):
if isinstance(other, xmlObj):
return (self.key == other.key
and self.xml_path == other.xml_path
and self.value == other.value
and self.languages == other.languages
and self.ios_key == other.ios_key
and self.ios_value == other.ios_value
and self.ios_path == other.ios_path
and self.old_value == other.old_value
and self.old_key == other.old_key)
return False
def __str__(self):
attrs = [
f"Key: {self.key}",
f"Value: {self.value}",
f"Old Value: {self.old_value}",
f"Old Key: {self.old_key}",
f"XML Path: {self.xml_path}",
f"iOS Key: {self.ios_key}",
f"iOS Value: {self.ios_value}",
f"iOS Path: {self.ios_path}",
f"Languages: {self.languages}"
]
return ', '.join(attrs)
class iosObj:
def __init__(self, key, value, path, androidKey, androidValue, androidPath, languages):
self.key = key
self.ios_path = path
self.value = value
self.androidKey = androidKey
self.androidValue = androidValue
self.androidPath = androidPath
self.languages = languages
def merge_xml_files(xml_files):
try:
title_path.add(xml_files.split("\\")[5])
tree = ET.parse(xml_files)
root = tree.getroot()
language_code = get_android_language(xml_files)
android_list = []
for child in root.findall("string"):
key = child.attrib.get("name")
value = child.text
android_list.append(xmlObj(key, value, '', '', xml_files, '', '', '', language_code))
return get_android_ios_obj(android_list)
except ET.ParseError as e:
print(f"Error parsing file {xml_files}: {e}")
def get_android_language(xml_files):
switch_dice = {
"zh-rMO": "TW",
"zh-rZH": "zh-rCN",
"pt-rBR": "pt",
"es-rLA": "es",
"el-rGR": "el",
"es-rES": "es",
}
direct_name = os.path.dirname(xml_files) # 获取目录路径 -> 'res/values-pt-rBR'
# 使用正则表达式提取语言标识(如 'pt-rBR')
match = re.search(r'values-([a-z]{2}(-[a-zA-Z0-9]{1,8})?)', direct_name)
if match:
language_code = match.group(1) # 输出: pt-rBR
count = language_code.count('-')
if count > 0:
language_code = switch_dice.get(language_code, language_code)
else:
language_code = "en"
return language_code
def get_ios_language(file_path):
direct_name = os.path.dirname(file_path)
language = os.path.basename(direct_name)
count = language.count('-')
if count == 0:
language = language.split('.')[0]
elif count == 1:
language = language.split('.')[0].split('-')[0]
elif count == 2:
language = language.split('.')[0].split('-')[2]
if language == "zh":
language = "zh-rCN"
elif language == "Base":
language = "en"
elif language == "CN":
language = "TW"
return language
def parse_strings_files(ios_files):
language = get_ios_language(ios_files)
pattern = re.compile(r'"([^"]+)"\s*=\s*"([^"]*)";')
with open(ios_files, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
match = pattern.match(line)
if match:
key = match.group(1)
value = match.group(2)
key = key.replace('\\', '"')
value = value.replace('\\', '"')
obj = iosObj(key, value, ios_files, '', '', '', language)
if "Resource" in ios_files:
ios_self_common.append(obj)
else:
ios_self_tai.append(obj)
ANDROID_FILE_PATTERN = re.compile(r'^strings(_v\d+)?\.xml$')
def is_valid_android_file(file_name: str) -> bool:
"""判断文件是否为 Android 类型,并且不等于 menustrings.commonstrings,country_strings"""
return (ANDROID_FILE_PATTERN.match(file_name) and
file_name != "menustrings.xml" and
file_name != "commonstrings.xml" and
file_name != "country_strings.xml")
def travel_root(root_path):
android_list = []
path_android = []
try:
for root, dirs, files in os.walk(root_path, followlinks=False):
valid_android = [os.path.join(root, file) for file in files
if is_valid_android_file(file) and
"Build" not in os.path.abspath(os.path.join(root, file)) and
file.endswith(tuple(suffixes_android)) and
contains_language(os.path.join(root, file))]
print(f"Directory: {root}, Android files: {valid_android}")
path_android.extend(valid_android)
[parse_strings_files(os.path.join(root, file)) for file in files if
file.endswith(tuple(suffixes_ios))]
print(f"Total Android files collected: {len(path_android)}")
with ThreadPoolExecutor(max_workers=12) as executor:
android_sublist = list(executor.map(merge_xml_files, path_android))
android_list.extend(android_sublist)
except Exception as e:
print(f"Error: {e}")
return android_list
def contains_language(path):
for key, value in language_vars.items():
if value.get() and key in path:
return True
return False
def get_android_ios_obj(android_list):
android_self_common = []
android_self_tai = []
for items_android in android_list:
if "main" in items_android.xml_path:
android_self_common.append(items_android)
else:
android_self_tai.append(items_android)
merge_xml_obj = merge_list(compare_android_ios(android_self_common, ios_self_common),
compare_android_ios(android_self_tai, ios_self_tai))
return merge_xml_obj
def merge_list(android_list_self_common, android_list_self_tai):
with lock:
merge_xml_obj = []
merge_xml_obj.extend(android_list_self_common + android_list_self_tai)
return merge_xml_obj
def is_same_language(android_value, ios_value):
if android_value.languages == ios_value.languages:
return True
else:
return False
def get_all_differ(android_copy, ios_self):
try:
android_by_key_and_value = defaultdict(list)
ios_by_key_and_value = defaultdict(list)
flatten_list = [item for sublist in android_copy for item in sublist]
for item in flatten_list:
if hasattr(item, "key") and hasattr(item, "value"):
key = f"{item.key}{item.value}"
android_by_key_and_value[key] = item
else:
print(f"无效对象,不包含 key 或 value 属性: {item}")
for item in ios_self:
if hasattr(item, "key") and hasattr(item, "value"):
key = f"{item.key}{item.value}"
ios_by_key_and_value[key] = item
else:
print(f"无效对象,不包含 key 或 value 属性: {item}")
for key in android_by_key_and_value:
if key not in ios_by_key_and_value:
item = android_by_key_and_value.get(key, 'default')
if item != 'default':
all_differ_key_value.append(item)
except Exception as e:
print(f"get_all_differ函数执行出错: {e}")
def compare_android_ios(android_self, ios_self):
print("正在比较......")
get_differ_key(android_self, ios_self)
android_copy = copy.deepcopy(android_self)
return get_differ_value(android_copy, ios_self)
def get_differ_key(android_self, ios_self):
try:
for self_android in android_self:
for self_ios in ios_self:
if self_android.value == self_ios.value and is_same_language(self_android, self_ios):
if self_android.key != self_ios.key:
old_key = self_android.key
self_android.key = self_ios.key
xml = xmlObj(self_android.key, self_android.value, self_android.value, old_key,
self_android.xml_path, self_ios.key,
self_ios.value, self_ios.ios_path, '')
queues_name['remediate_list'].put(xml)
except Exception as e:
print(f"get_differ_key 报错:{e}")
def get_differ_value(android_copy, ios_self):
for self_android in android_copy:
for self_ios in ios_self:
if self_android.key == self_ios.key and is_same_language(self_android, self_ios):
if self_ios.value != self_android.value:
self_android.value = self_ios.value
xml = xmlObj(self_android.key, self_android.value, self_android.old_value, self_android.old_key,
self_android.xml_path, self_ios.key,
self_ios.value, self_ios.ios_path, '')
queues_name['remediate_list'].put(xml)
return copy.deepcopy(android_copy)
def write_dict_to_xml(data_dict):
try:
group_data = defaultdict(list)
for obj in data_dict:
for item in obj:
group_data[item.xml_path].append(item)
for path, items in group_data.items():
resource = ET.Element('resources')
for item in items:
et = ET.SubElement(
resource,
"string",
{"name": item.key}
)
et.text = item.value
tree = ET.ElementTree(resource)
ET.indent(tree, space=" ") # 使 XML 格式整齐
tree.write(path, encoding="utf-8", xml_declaration=True, method='xml')
print(f"写入文件中{path}请稍等")
except IOError as e:
print(f"写入xml文件操作失败({path}): {str(e)}")
except AttributeError as e:
print(f"写入xml对象属性缺失: {str(e)}")
except ET.ParseError as e:
print(f"写入XML格式错误: {str(e)}")
except Exception as e:
print(f"写入xml未知错误: {str(e)}")
def write_to_excel(android_list_self, is_same, path):
try:
print("正在写入excel")
wb = Workbook()
for item in android_list_self:
title = get_parent_directory(item.xml_path)
for elem in title_path:
if elem in item.xml_path:
title = elem
# 根据 title 获取或创建工作表
if title in wb.sheetnames:
ws = wb[title]
else:
ws = wb.create_sheet(title=title)
if is_same:
ws.append(
["old_key", "old_value", "ios_key", "ios_value", "ios_path", "android_path", "key", "value"])
else:
ws.append(
["old_key", "old_value", "ios_key", "ios_value", "ios_path", "android_path", "key", "value"])
# 将数据追加到对应工作表
ws.append(
[item.old_key, item.old_value, item.ios_key, item.ios_value, item.ios_path, item.xml_path, item.key,
item.value])
if is_same:
file_path = os.path.join(path, 'android_same.xlsx')
else:
file_path = os.path.join(path, 'android_not_same.xlsx')
file_path = os.path.join(path, file_path)
wb.save(file_path)
except PermissionError as e:
print(f"文件保存失败:权限不足 ({str(e)})")
except FileNotFoundError as e:
print(f"文件路径无效:路径不存在 ({str(e)})")
except openpyxl.utils.exceptions.IllegalCharacterError as e:
print(f"Excel文件数据异常:包含非法字符 ({str(e)})")
except AttributeError as e:
print(f"对象属性错误:请检查数据完整性 ({str(e)})")
def get_parent_directory(xml_path):
path = Path(xml_path)
parts = path.parts
for i in range(len(parts) - 1):
if parts[i].lower() == 'res':
return path.resolve().parts[i - 1]
return None
is_all_selected = False
def on_submit():
selected = [lang for lang, var in language_vars.items() if var.get()]
print(f"你选择了: {selected}")
root.destroy()
def toggle_all():
global is_all_selected
is_all_selected = not is_all_selected
for var in language_vars.values():
var.set(is_all_selected)
# 初始化窗口
root = tk.Tk()
root.title("语言选择")
root.geometry("400x800")
# 语言选项(使用语言代码)
languages = ['ar', 'ca', 'de', 'el', 'en', 'enm', 'es-rES', 'es-rLA', 'eu', 'fr', 'pt-rBR', 'ru', 'th', 'zh-rCN',
'zh-rMO', 'zh-rZH']
# 全选按钮
ttk.Button(root, text="全选", command=toggle_all).grid(row=len(languages) + 1, column=0, padx=20, pady=20)
# 提交按钮
ttk.Button(root, text="提交", command=on_submit).grid(row=len(languages) + 1, column=1, padx=20, pady=20)
# 复选框按钮
for idx, lang in enumerate(languages):
var = tk.BooleanVar()
ttk.Checkbutton(root, text=lang, variable=var).grid(row=idx, sticky="ew", padx=10, pady=5)
language_vars[lang] = var
# 启动事件循环
root.mainloop()
if __name__ == '__main__':
android_map = {}
android_root_path = input("请输入Android路径:")
ios_root_path = input("请输入ios路径:")
travel_root(ios_root_path)
key_value_list_android = travel_root(android_root_path)
ios_self_common.extend(ios_self_tai)
get_all_differ(key_value_list_android, ios_self_common)
data_dict = list(queues_name['remediate_list'].queue)
thread_remediate_list = threading.Thread(target=write_to_excel, args=(data_dict, True, android_root_path))
thread_all_differ_key_value = threading.Thread(target=write_to_excel,
args=(all_differ_key_value, False, android_root_path))
thread_remediate_list.start()
thread_all_differ_key_value.start()
write_dict_to_xml(key_value_list_android)