Android项目资源字符串内容多语言对齐工具:
#!/usr/bin/env python3import re
from dataclasses import dataclass, field
from typing import Optional, Dict, List
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox@dataclass
class StringLine:name: Optional[str]raw: strtext: Optional[str] = Noneattrs: Dict[str, str] = field(default_factory=dict)def parse_string_lines(xml_path: Path) -> List[StringLine]:lines = []last_line_empty = Falsestring_tag_pattern = re.compile(r'<string\s+([^>]+)>(.*?)</string>', re.DOTALL)attr_pattern = re.compile(r'(\w+)="(.*?)"')with xml_path.open(encoding='utf-8') as f:for line in f:stripped = line.strip()if not stripped:if not last_line_empty:lines.append(StringLine(name=None, raw="\n"))last_line_empty = Truecontinuelast_line_empty = Falsematch = string_tag_pattern.match(stripped)if match:attr_text, content = match.groups()attrs = dict(attr_pattern.findall(attr_text))name = attrs.get("name")lines.append(StringLine(name=name, raw=line, text=content, attrs=attrs))else:lines.append(StringLine(name=None, raw=line))return linesdef map_string_lines(lines: List[StringLine]) -> Dict[str, StringLine]:return {line.name: line for line in lines if line.name}def align_string_lines(base_lines: List[StringLine], target_lines: List[StringLine]) -> List[StringLine]:target_map = map_string_lines(target_lines)aligned_lines = []for line in base_lines:if line.name:target_line = target_map.pop(line.name, None)aligned_lines.append(target_line if target_line else line)else:aligned_lines.append(line)if target_map:aligned_lines.append(StringLine(name=None, raw="\n"))aligned_lines.append(StringLine(name=None, raw="\n"))aligned_lines.extend(target_map.values())return aligned_linesdef write_aligned_lines(lines: List[StringLine], output_path: Path):with output_path.open('w', encoding='utf-8') as f:# f.write('<resources>\n')for line in lines:f.write(line.raw.rstrip('\n') + '\n')# f.write('</resources>\n')class AlignStringsApp:def __init__(self, root):self.root = rootself.root.title("Android 资源文件多语言字符串对齐工具-WKF")self.root.resizable(False, False)# 计算居中坐标win_width, win_height = 916, 190x = (self.root.winfo_screenwidth() - win_width) // 2y = (self.root.winfo_screenheight() - win_height) // 2self.root.geometry(f"{win_width}x{win_height}+{x}+{y}")self.base_file = Noneself.res_dir = Noneself.output_dir = Nonetk.Label(root, text="基准 strings.xml 文件:").grid(row=0, column=0, sticky="w", padx=(10, 0), pady=(16, 0))self.base_entry = tk.Entry(root, width=100, state="readonly")self.base_entry.grid(row=0, column=1, pady=(16, 0))tk.Button(root, text="选择", width=6, command=self.select_base_file).grid(row=0, column=2, pady=(16, 0))tk.Label(root, text="res 根目录:").grid(row=1, column=0, sticky="w", padx=(10, 0))self.res_entry = tk.Entry(root, width=100, state="readonly")self.res_entry.grid(row=1, column=1)tk.Button(root, text="选择", width=6, command=self.select_res_dir).grid(row=1, column=2)tk.Label(root, text="输出目录:").grid(row=2, column=0, sticky="w", padx=(10, 0))self.out_entry = tk.Entry(root, width=100, state="readonly")self.out_entry.grid(row=2, column=1)tk.Button(root, text="选择", width=6, command=self.select_output_dir).grid(row=2, column=2)self.align_button = tk.Button(root, text="一键对齐", width=16, height=2, command=self.align_strings)self.align_button.grid(row=3, column=1, pady=10)messagebox.showinfo("提示","本工具以选择的xml文件为基准对齐内容,对齐的文件中若不存在则补充基准的内容,若多了则保留在处理后的文件末尾!")def select_base_file(self):path = filedialog.askopenfilename(filetypes=[("XML 文件", "*.xml")])if path:self.base_file = Path(path)self.base_entry.config(state="normal")self.base_entry.delete(0, tk.END)self.base_entry.insert(0, str(self.base_file))self.base_entry.config(state="readonly")def select_res_dir(self):path = filedialog.askdirectory()if path:self.res_dir = Path(path)self.res_entry.config(state="normal")self.res_entry.delete(0, tk.END)self.res_entry.insert(0, str(self.res_dir))self.res_entry.config(state="readonly")def select_output_dir(self):path = filedialog.askdirectory()if path:self.output_dir = Path(path)self.out_entry.config(state="normal")self.out_entry.delete(0, tk.END)self.out_entry.insert(0, str(self.output_dir))self.out_entry.config(state="readonly")def align_strings(self):# 判断是否路径都已选择if not self.base_file or not self.base_file.exists():messagebox.showwarning("警告", "请先选择【基准 strings.xml 文件】")returnif not self.res_dir or not self.res_dir.exists():messagebox.showwarning("警告", "请先选择【res 根目录】")returnif not self.output_dir or not self.output_dir.exists():messagebox.showwarning("警告", "请先选择【输出目录】")returntry:base_lines = parse_string_lines(self.base_file)base_name = self.base_file.namefor xml_path in self.res_dir.rglob(base_name):target_lines = parse_string_lines(xml_path)aligned = align_string_lines(base_lines, target_lines)relative = xml_path.relative_to(self.res_dir)out_path = self.output_dir / relativeout_path.parent.mkdir(parents=True, exist_ok=True)write_aligned_lines(aligned, out_path)messagebox.showinfo("完成", "对齐完成!")except Exception as e:messagebox.showerror("错误", f"对齐失败: {e}")if __name__ == "__main__":window = tk.Tk()app = AlignStringsApp(window)window.mainloop()
工具运行截图: