synxml.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import re
  2. import os
  3. import glob
  4. import fileinput
  5. import xml.etree.ElementTree as ET
  6. import pygsheets
  7. from optparse import OptionParser
  8. import logging
  9. logging.basicConfig(level=logging.DEBUG)
  10. def CDATA(text=None):
  11. element = ET.Element('![CDATA[')
  12. element.text = text
  13. return element
  14. ET._original_serialize_xml = ET._serialize_xml
  15. #CDATA特殊处理
  16. def _serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs):
  17. if 'name' in elem.attrib.keys() and elem.attrib['name'] in andorid_contian_cdata_keys:
  18. write("<{} name=\"{}\"><![CDATA[{}]]></{}>{}".format(elem.tag,elem.attrib['name'],elem.text,elem.tag,elem.tail))
  19. else:
  20. return ET._original_serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs)
  21. ET._serialize_xml = ET._serialize['xml'] = _serialize_xml
  22. andorid_root_directory = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))
  23. ios_root_directory = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))
  24. ios_languages = ['ar', 'en', 'hi', 'ta', 'te', 'th', 'tr', 'zh-Hans']
  25. #andorid_language = ['', '-ar', '-hi', '-ta', '-te', '-th', '-tr', '-zh']
  26. # andorid_language = ['', '-ar','-hi','-bn','-es','-in','-ms','-pt','-th','-tl','-tr','-ur','-vi','-zh','-zh-TW']ru kk ky tk tg uz
  27. andorid_language = ['','-zh', '-ar']
  28. #xml对CDATA解析有点问题, CDATA 数据需要在这里添加key手动处理
  29. andorid_contian_cdata_keys = ['room_high_potential_conversion_reward','ludo_exit_game_tip','message_follow',
  30. 'call_send_diamond','call_end_chat_get_diamonds','call_end_chat_get_coins','call_end_chat_un_get_diamonds','call_end_chat_un_get_coins',
  31. 'couple_love_house_no_couple_desc','game_rules_2','game_rocket_reward_record','message_follow',
  32. 'room_rank_list_title','wallet_diamond_balance','wallet_coin_balance','common_cp_level_not_reach','domino_exit_game_tip','ludo_load_game_go']
  33. def find_strings_files(root_dir,lan = "en"):
  34. translation_files = [f for f in glob.glob(os.path.join(root_dir, '**', lan + '.lproj', 'Localizable.strings'), recursive=True) if 'MJRefresh' not in f]
  35. return translation_files
  36. def find_xml_files(root_dir = andorid_root_directory,lan = ""):
  37. translation_files = glob.glob(os.path.join(root_dir, '**', 'res/values'+lan, 'strings.xml'), recursive=True)
  38. return translation_files
  39. def lan_of_path(path):
  40. parts = path.split('/')
  41. lan = parts[-2].strip('.lproj')
  42. return lan
  43. def convert_ios_strings(value):
  44. placeholder_pattern = r'%\d*\$?[sdf]'
  45. if len(re.findall(placeholder_pattern,value)) > 0:
  46. value = value.replace('%s', '%@')
  47. value = re.sub(r'%(\d+)\$s', r'%\1$@', value)
  48. return value
  49. def map_andorid_lan(lan):
  50. if lan == 'en':
  51. return ''
  52. elif lan == 'zh-Hans':
  53. return '-zh'
  54. else:
  55. return '-'+lan
  56. def map_sheet_lan(lan):
  57. if lan == 'zh-Hans':
  58. return 'zh'
  59. else:
  60. return lan
  61. def extract_xml_to_dic(lan = ''):
  62. count = 0
  63. translations = {}
  64. translation_files = find_xml_files(andorid_root_directory,lan)
  65. android_placeholder_pattern = r'%\d*\$?[sdf]'
  66. for fname in translation_files:
  67. print(f"Processing file: {fname}")
  68. tree = ET.parse(fname)
  69. root = tree.getroot()
  70. for child in root:
  71. key = child.attrib['name']
  72. value = child.text
  73. if len(re.findall(android_placeholder_pattern,value)) > 0:
  74. value = value.replace('%s', '%@')
  75. value = re.sub(r'%(\d+)\$s', r'%\1$@', value)
  76. translations[key] = value
  77. else:
  78. translations[key] = value
  79. return translations
  80. def read_into_table(filelist):
  81. table = {}
  82. with fileinput.input(files=filelist) as f:
  83. for line in f:
  84. if "=" in line:
  85. key, value = line.split("=", 1)
  86. table[key.strip()[1:-1]] = value.strip()[1:-2]
  87. return table
  88. def read_xml_into_table(file):
  89. table = {}
  90. with fileinput.input(files=[file]) as f:
  91. tree = ET.parse(file)
  92. root = tree.getroot()
  93. for child in root:
  94. key = child.attrib['name']
  95. value = child.text
  96. table[key] = value
  97. return table
  98. def read_xml_to_table(lan = ''):
  99. count = 0
  100. translations = {}
  101. translation_files = find_xml_files(andorid_root_directory,lan)
  102. print(f"Processing read_xml_to_table lan: {lan}")
  103. for fname in translation_files:
  104. tree = ET.parse(fname)
  105. root = tree.getroot()
  106. for child in root:
  107. key = child.attrib['name']
  108. value = child.text
  109. # print(f'key ==> {key}')
  110. translations[key] = value
  111. return translations
  112. def mergexml(lan = ""):
  113. # 创建一个 ElementTree 用于存储合并后的内容
  114. merged_tree = ET.ElementTree(ET.Element('resources'))
  115. # 遍历根目录及其子目录
  116. for root, dirs, files in os.walk(andorid_root_directory):
  117. for filename in files:
  118. if filename == 'strings.xml' and root.endswith("res/values"+lan):
  119. xml_file_path = os.path.join(root, filename)
  120. # print(xml_file_path)
  121. # 解析当前 XML 文件
  122. tree = ET.parse(xml_file_path)
  123. root_elem = tree.getroot()
  124. # 将当前 XML 文件的内容添加到合并后的 XML 文件中
  125. for elem in root_elem:
  126. merged_tree.getroot().append(elem)
  127. # 将合并后的内容写入总的 strings.xml 文件
  128. outfilename = 'merged_strings'+lan +'.xml'
  129. outfilepath = os.path.join('MergedXml', outfilename)
  130. merged_tree.write(outfilepath, encoding='utf-8', xml_declaration=True)
  131. print(f"{lan} 合并完成 \n")
  132. def findallxml():
  133. # 遍历根目录及其子目录
  134. for root, dirs, files in os.walk(andorid_root_directory):
  135. for filename in files:
  136. if filename == 'strings.xml' and "res/values" in root:
  137. # 找到 strings.xml 文件
  138. xml_file_path = os.path.join(root, filename)
  139. print(f"找到 strings.xml 文件:{xml_file_path}")
  140. # print(f'{filename}')
  141. def find_all_xml_files():
  142. for lan in andorid_language:
  143. find_xml_files(lan=lan)
  144. def merge_all_files():
  145. for lan in andorid_language:
  146. mergexml(lan=lan)
  147. def find_untrans_items():
  148. untransItems = {}
  149. files = find_strings_files(root_dir=ios_root_directory)
  150. for filename in files:
  151. en_tables = read_into_table(filename)
  152. ar_tables = read_into_table(filename.replace('en.lproj','ar.lproj'))
  153. result = set(en_tables.keys()) - set(ar_tables.keys())
  154. if len(result) > 0 :
  155. print(f'{filename} , untranslate keys: {result}')
  156. for key in result:
  157. print(f'{key} ==> {en_tables[key]}')
  158. untransItems[key] = en_tables[key]
  159. return untransItems
  160. def find_all_untrans_items():
  161. untransItems = {}
  162. files = find_strings_files(root_dir=ios_root_directory)
  163. for filename in files:
  164. en_tables = read_into_table(filename)
  165. ar_tables = read_into_table(filename.replace('en.lproj','ar.lproj'))
  166. result = set(en_tables.keys()) - set(ar_tables.keys())
  167. if len(result) > 0 :
  168. print(f'{filename} , untranslate keys: {result}')
  169. for key in result:
  170. print(f'{key} ==> {en_tables[key]}')
  171. untransItems[key] = en_tables[key]
  172. return untransItems
  173. def syn_translate_ios(transItems = {}):
  174. if len(transItems) > 0:
  175. files = find_strings_files(root_dir=ios_root_directory)
  176. for filename in files:
  177. en_tables = read_into_table(filename)
  178. processkeys = set(en_tables.keys()) & set(transItems.keys())
  179. if len(processkeys) > 0 :
  180. for lan in ios_languages:
  181. lanfile = filename.replace('en.lproj',f'{lan}.lproj')
  182. print(f'开始处理文件: {lanfile}')
  183. preparekey = list(processkeys)
  184. with open(lanfile, 'r', encoding='utf-8') as infile:
  185. lines = infile.readlines()
  186. with open(lanfile, 'w', encoding='utf-8') as outfile:
  187. for line in lines:
  188. if '=' in line:
  189. key, value = line.strip().split("=", 1)
  190. key = key.strip()[1:-1]
  191. value = value.strip()[1:-2]
  192. if key in transItems.keys():
  193. newvalue = convert_ios_strings(transItems[key][map_sheet_lan(lan)]).strip()
  194. if newvalue == '':
  195. newvalue = transItems[key]['en']
  196. if newvalue != value:
  197. modified_line = f'"{key}" = "{newvalue}";\n'
  198. outfile.write(modified_line)
  199. if key in preparekey:
  200. preparekey.remove(key)
  201. print(f'更新 ==>[{lan}:{key}] {value} ==> {newvalue}')
  202. else:
  203. outfile.write(line)
  204. if key in preparekey:
  205. preparekey.remove(key)
  206. else:
  207. outfile.write(line)
  208. for key in preparekey:
  209. newvalue = convert_ios_strings(transItems[key][map_sheet_lan(lan)]).strip()
  210. if newvalue == '':
  211. print(f'!!添加 ==>[{lan}:{key}] 翻译为空,默认使用英文翻译')
  212. newvalue = transItems[key]['en']
  213. newline = f'"{key}" = "{newvalue}";\n'
  214. outfile.write(newline)
  215. print(f'添加 ==>[{lan}:{key}] ==> {newvalue}')
  216. print(f'同步完成 \n')
  217. else:
  218. print(f'未找到英文翻译中的差异的 key \n')
  219. else:
  220. print('没有需要同步的表格数据')
  221. def syn_translate_andorid(transItems = {}):
  222. if len(transItems) > 0:
  223. files = find_xml_files(root_dir=andorid_root_directory)
  224. for filename in files:
  225. en_tables = read_xml_into_table(filename)
  226. processkeys = set(en_tables.keys()) & set(transItems.keys())
  227. if len(processkeys) > 0 :
  228. for lan in andorid_language:
  229. lan_key = lan.replace('-','')
  230. if lan_key == '':
  231. lan_key = 'en'
  232. if lan=='-zh-TW':
  233. lanfile = filename.replace('res/values',f'res/values-zh-rTW')
  234. lan_key = 'zh-TW'
  235. else :
  236. lanfile = filename.replace('res/values',f'res/values{lan}')
  237. # 检查目录是否存在
  238. lanfile_dir = os.path.dirname(lanfile)
  239. if not os.path.exists(lanfile_dir):
  240. os.makedirs(lanfile_dir) # 创建目录
  241. print(f"目录 {lanfile_dir} 已创建")
  242. # 检查文件是否存在
  243. if not os.path.exists(lanfile):
  244. with open(lanfile, 'w', encoding='utf-8') as f:
  245. f.write('<?xml version="1.0" encoding="utf-8"?>\n<resources>\n</resources>') # 可以初始化文件内容
  246. print(f"文件 {lanfile} 已创建")
  247. else:
  248. print(f"文件 {lanfile} 已存在")
  249. print(lan)
  250. print(lan_key)
  251. print(lanfile)
  252. print(f'开始处理文件: {lanfile}')
  253. preparekey = list(processkeys)
  254. tree = ET.parse(lanfile)
  255. root = tree.getroot()
  256. for child in root:
  257. key = child.attrib['name']
  258. value = child.text
  259. print(key)
  260. print(value)
  261. if key in transItems.keys():
  262. newvalue = str(transItems[key][lan_key]).strip()
  263. print(111)
  264. print(newvalue)
  265. if newvalue == '':
  266. newvalue = transItems[key]['en']
  267. if newvalue != value:
  268. child.text = newvalue
  269. print(f'更新 ==>[{lan}:{key}] {value} ==> {newvalue}')
  270. if key in preparekey:
  271. preparekey.remove(key)
  272. else:
  273. child.text = value
  274. for key in preparekey:
  275. newvalue = str(transItems[key][lan_key]).strip()
  276. if newvalue == '':
  277. print(f'!!添加 ==>[{lan}:{key}] 翻译为空,默认使用英文翻译')
  278. newvalue = transItems[key]['en']
  279. new_string_element = ET.Element('string')
  280. new_string_element.set('name', key)
  281. new_string_element.text = newvalue
  282. root.append(new_string_element)
  283. print(f'添加 ==>[{lan}:{key}] ==> {newvalue}')
  284. ET.indent(tree, ' ')
  285. tree.write(lanfile, encoding='utf-8',xml_declaration=False)
  286. #write("<?xml version='1.0' encoding='%s'?>\n" % (declared_encoding,))
  287. with open(lanfile,'r+') as f:
  288. file_data = f.read()
  289. f.seek(0, 0)
  290. f.write('<?xml version="1.0" encoding="utf-8"?>\n' + file_data)
  291. print(f'同步完成 \n')
  292. else:
  293. print('没有需要同步的表格数据')
  294. class syntransition:
  295. def __init__(self,fileName = 'Ludo Transaltion',export = False, platform = 1,title = ''):
  296. self.client = pygsheets.authorize(service_file = "localizable-key.json")
  297. self.sheet = self.client.open(fileName)
  298. self.platform = platform
  299. # 尝试查找工作表,如果找不到,则添加一个新工作表
  300. try:
  301. self.worksheet = self.sheet.worksheet_by_title(title)
  302. except pygsheets.exceptions.WorksheetNotFound:
  303. if export:
  304. # 如果工作表不存在,添加一个新工作表
  305. template_wks = self.sheet.worksheet_by_title('template')
  306. self.worksheet = self.sheet.add_worksheet(title=title, src_worksheet=template_wks)
  307. else:
  308. print('工作表不存在,请检查sheet名是否正确')
  309. def read_sheet(self):
  310. tables = {}
  311. for row in self.worksheet.get_all_records():
  312. key = 'i_key' if self.platform == 2 else 'a_key'
  313. trans_key = row[key]
  314. if trans_key.strip() != '':
  315. tables[trans_key] = row
  316. return tables
  317. def syn_translate(filename, title,platform,):
  318. #'消息设置页面'
  319. syn = syntransition(fileName=filename,title=title,platform=platform)
  320. translations = syn.read_sheet()
  321. print(translations)
  322. if platform == 1:
  323. syn_translate_andorid(translations)
  324. else:
  325. syn_translate_ios(translations)
  326. def addParser():
  327. parser = OptionParser()
  328. parser.add_option("-f", "--fileName",
  329. default="Ludo Transaltion",
  330. help="表文件名称",
  331. metavar="fileName")
  332. parser.add_option("-t", "--sheetTitle",
  333. help="google sheet表格名",
  334. metavar="sheetTitle")
  335. parser.add_option("-p", "--platform",
  336. default=1,
  337. help="平台(andorid: 1,ios :2 )",
  338. metavar="platform")
  339. (options, args) = parser.parse_args()
  340. print("options: %s, args: %s" % (options, args))
  341. return options
  342. def startConvert(options):
  343. filename = options.fileName
  344. sheetTitle = options.sheetTitle
  345. platform = options.platform
  346. print("Start converting")
  347. if platform != 1:
  348. platform = 2
  349. if sheetTitle is None:
  350. print("google sheet表格名不能为空")
  351. return
  352. syn_translate(filename=filename,title=sheetTitle,platform=1)
  353. if __name__ == '__main__':
  354. options = addParser()
  355. startConvert(options)
  356. #syn_translate(filename='Ludo Transaltion',title='ludo排行榜',platform=1)
  357. # 使用:表名如有空格,需要用''标识sheet表名,synxml.py -t 'ludo 排行榜'
  358. # synxml.py -t ludo排行榜