import sys import io import os import xml.etree.ElementTree as ET import re # Define namespaces URIs XAML_NS = 'https://github.com/avaloniaui' X_NS = 'http://schemas.microsoft.com/winfx/2006/xaml' def register_namespaces(): """Registers namespaces for ElementTree to use when writing the XML file.""" ET.register_namespace('', XAML_NS) ET.register_namespace('x', X_NS) def get_locale_dir(): """Constructs the absolute path for the locales files""" try: script_dir = os.path.dirname(os.path.realpath(__file__)) project_root = os.path.abspath(os.path.join(script_dir, '.')) locales_dir = os.path.join(project_root, 'src', 'Resources', 'Locales') except NameError: project_root = os.path.abspath(os.getcwd()) locales_dir = os.path.join(project_root, 'src', 'Resources', 'Locales') return locales_dir def get_locale_files(lang_id): """Constructs the absolute paths for the target and reference locale files.""" # get the locales dir locales_dir = get_locale_dir() # get the target file absolute path target_file = os.path.join(locales_dir, f"{lang_id}.axaml") if not os.path.exists(target_file): print(f"Error: Target language file not found at {target_file}") sys.exit(1) try: tree = ET.parse(target_file) root = tree.getroot() merged_dict = root.find(f"{{{XAML_NS}}}ResourceDictionary.MergedDictionaries") if merged_dict is None: raise ValueError("Could not find MergedDictionaries tag.") resource_include = merged_dict.find(f"{{{XAML_NS}}}ResourceInclude") if resource_include is None: raise ValueError("Could not find ResourceInclude tag.") include_source = resource_include.get('Source') ref_filename_match = re.search(r'([a-zA-Z]{2}_[a-zA-Z]{2}).axaml', include_source) if not ref_filename_match: raise ValueError("Could not parse reference filename from Source attribute.") ref_filename = f"{ref_filename_match.group(1)}.axaml" ref_file = os.path.join(locales_dir, ref_filename) except Exception as e: print(f"Error parsing {target_file} to find reference file: {e}") sys.exit(1) if not os.path.exists(ref_file): print(f"Error: Reference language file '{ref_file}' not found.") sys.exit(1) return target_file, ref_file def get_strings(root): """Extracts all translation keys and their text values from an XML root.""" strings = {} for string_tag in root.findall(f"{{{X_NS}}}String"): key = string_tag.get(f"{{{X_NS}}}Key") if key: strings[key] = string_tag.text if string_tag.text is not None else "" return strings def add_new_string_tag(root, key, value): """Adds a new tag to the XML root, maintaining some formatting.""" # create a new tag with Key<>Value new_tag = ET.Element(f"{{{X_NS}}}String") new_tag.set(f"{{{X_NS}}}Key", key) new_tag.set("xml:space", "preserve") new_tag.text = value # try to find the last tag in the last_element_index = -1 children = list(root) start = 0 end = len(children) - 1 middle = -1 # find the first String tag while (children[start].tag != f"{{{X_NS}}}String"): start = start + 1 # find where the insert the new tag # so it always keeps the alphabetical order while (end - start) > 1: middle = int((start + end) / 2) if (children[middle].tag == f"{{{X_NS}}}String"): middle_key = children[middle].get(f"{{{X_NS}}}Key") if key.lower() < middle_key.lower(): end = middle else: start = middle # insert after the middle or at the end if middle != -1: new_tag.tail = root[middle].tail root.insert(middle + 1, new_tag) else: new_tag.tail = "\n " root.append(new_tag) def save_translations(tree, file_path): """Saves the XML tree to the file, attempting to preserve formatting.""" try: ET.indent(tree, space=" ") except AttributeError: print("Warning: ET.indent not available. Output formatting may not be ideal.") tree.write(file_path, encoding='utf-8', xml_declaration=False) print(f"\nSaved changes to {file_path}") def main(): """Main function to run the translation helper script.""" if len(sys.argv) < 2: print("Usage: python utils/translate_helper.py [--check]") sys.exit(1) # Force sys.stdin to use UTF-8 decoding if sys.stdin.encoding.lower() != 'utf-8': print("Changin input encoding to UTF-8") sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') # get arguments lang_id = sys.argv[1] is_check_mode = len(sys.argv) > 2 and sys.argv[2] == '--check' # setup XML parser register_namespaces() # try to find XML files target_file_path, ref_file_path = get_locale_files(lang_id) # parse files parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True)) target_tree = ET.parse(target_file_path, parser) target_root = target_tree.getroot() ref_tree = ET.parse(ref_file_path) ref_root = ref_tree.getroot() # get all translation keys target_strings = get_strings(target_root) ref_strings = get_strings(ref_root) # compute the missing keys between the target and reference files missing_keys = sorted([key for key in ref_strings.keys() if key not in target_strings]) if not missing_keys: print("All keys are translated. Nothing to do.") return print(f"Found {len(missing_keys)} missing keys for language '{lang_id}'.") # running in check mode will only display the missing keys if is_check_mode: print("Missing keys:") for key in missing_keys: print(f" - {key}") return # running in normal mode will trigger the translation process print("Starting interactive translation...\n") changes_made = False try: # for each missing key for i, key in enumerate(missing_keys): # show the original text original_text = ref_strings.get(key, "") print("-" * 40) print(f"({i+1}/{len(missing_keys)}) Key: '{key}'") print(f"Original: '{original_text}'") # asks for a translated version user_input = input("Enter translation (or press Enter to skip, 'q' to save and quit): ") # if 'q' quit and save if user_input.lower() == 'q': print("\nQuitting and saving changes...") break # if valid input, save elif user_input: add_new_string_tag(target_root, key, user_input) changes_made = True print(f"Added translation for '{key}'") except (KeyboardInterrupt, EOFError): print("\n\nProcess interrupted. Saving changes...") finally: # if there was any changes, save back to the target file if changes_made: save_translations(target_tree, target_file_path) else: print("\nNo changes were made.") if __name__ == "__main__": main()