From a15ca9f856f47f22407e4f43e9d6039048480cc1 Mon Sep 17 00:00:00 2001 From: Ramon Melo Date: Sat, 27 Sep 2025 09:00:01 -0300 Subject: [PATCH 1/4] feature: Translation helper script A small python scritp that helps to translate between EN to a target language. It loads the target translation file and compares with the EN base file, showing which keys are missing and asking to translate. --- utils/translate_helper.py | 165 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 utils/translate_helper.py diff --git a/utils/translate_helper.py b/utils/translate_helper.py new file mode 100644 index 000000000..b5903cb68 --- /dev/null +++ b/utils/translate_helper.py @@ -0,0 +1,165 @@ +import sys +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_files(lang_id): + """Constructs the absolute paths for the target and reference locale 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') + + 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.""" + 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 + + last_element_index = -1 + children = list(root) + for i in range(len(children) - 1, -1, -1): + if (children[i].tag == f"{{{X_NS}}}String" or + children[i].tag == f"{{{XAML_NS}}}ResourceDictionary.MergedDictionaries"): + last_element_index = i + break + + if last_element_index != -1: + new_tag.tail = root[last_element_index].tail + root.insert(last_element_index + 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=True) + print(f"\nSaved changes to {file_path}") + +def main(): + """Main function to run the translation helper script.""" + register_namespaces() + + if len(sys.argv) < 2: + print("Usage: python utils/translate_helper.py [--check]") + sys.exit(1) + + lang_id = sys.argv[1] + is_check_mode = len(sys.argv) > 2 and sys.argv[2] == '--check' + + target_file_path, ref_file_path = get_locale_files(lang_id) + + 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() + + target_strings = get_strings(target_root) + ref_strings = get_strings(ref_root) + + 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}'.") + + if is_check_mode: + print("Missing keys:") + for key in missing_keys: + print(f" - {key}") + return + + print("Starting interactive translation...\n") + changes_made = False + try: + for i, key in enumerate(missing_keys): + original_text = ref_strings.get(key, "") + print("-" * 40) + print(f"({i+1}/{len(missing_keys)}) Key: '{key}'") + print(f"Original: '{original_text}'") + + user_input = input("Enter translation (or press Enter to skip, 'q' to save and quit): ") + + if user_input.lower() == 'q': + print("\nQuitting and saving changes...") + break + 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 changes_made: + save_translations(target_tree, target_file_path) + else: + print("\nNo changes were made.") + +if __name__ == "__main__": + main() \ No newline at end of file From a37d4f0110bd41c8ac3eb094bdeef87a9fe4c6bd Mon Sep 17 00:00:00 2001 From: Ramon Melo Date: Sat, 27 Sep 2025 10:04:43 -0300 Subject: [PATCH 2/4] feature: Added code comments to translation utility script --- utils/translate_helper.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/utils/translate_helper.py b/utils/translate_helper.py index b5903cb68..c432247ce 100644 --- a/utils/translate_helper.py +++ b/utils/translate_helper.py @@ -12,8 +12,8 @@ def register_namespaces(): ET.register_namespace('', XAML_NS) ET.register_namespace('x', X_NS) -def get_locale_files(lang_id): - """Constructs the absolute paths for the target and reference locale files.""" +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, '..')) @@ -22,6 +22,15 @@ def get_locale_files(lang_id): 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): @@ -67,11 +76,14 @@ def get_strings(root): 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) for i in range(len(children) - 1, -1, -1): @@ -99,27 +111,33 @@ def save_translations(tree, file_path): def main(): """Main function to run the translation helper script.""" - register_namespaces() - if len(sys.argv) < 2: print("Usage: python utils/translate_helper.py [--check]") sys.exit(1) + # 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: @@ -128,26 +146,34 @@ def main(): 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 @@ -156,10 +182,11 @@ def main(): 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() \ No newline at end of file + main() From af76dc8b44879845481106e800b5d9af19cfb006 Mon Sep 17 00:00:00 2001 From: Ramon Melo Date: Sat, 27 Sep 2025 11:34:30 -0300 Subject: [PATCH 3/4] feature: Insert new keys in alphabetical order when translating --- utils/translate_helper.py | 43 ++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/utils/translate_helper.py b/utils/translate_helper.py index c432247ce..7e294f8d7 100644 --- a/utils/translate_helper.py +++ b/utils/translate_helper.py @@ -1,4 +1,5 @@ import sys +import io import os import xml.etree.ElementTree as ET import re @@ -86,15 +87,32 @@ def add_new_string_tag(root, key, value): # try to find the last tag in the last_element_index = -1 children = list(root) - for i in range(len(children) - 1, -1, -1): - if (children[i].tag == f"{{{X_NS}}}String" or - children[i].tag == f"{{{XAML_NS}}}ResourceDictionary.MergedDictionaries"): - last_element_index = i - break - - if last_element_index != -1: - new_tag.tail = root[last_element_index].tail - root.insert(last_element_index + 1, new_tag) + + 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) @@ -106,7 +124,7 @@ def save_translations(tree, file_path): except AttributeError: print("Warning: ET.indent not available. Output formatting may not be ideal.") - tree.write(file_path, encoding='utf-8', xml_declaration=True) + tree.write(file_path, encoding='utf-8', xml_declaration=False) print(f"\nSaved changes to {file_path}") def main(): @@ -115,6 +133,11 @@ def main(): 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' From 7be9ba91f051cc99191cf897e021f8d4b656a355 Mon Sep 17 00:00:00 2001 From: Ramon Melo Date: Sat, 27 Sep 2025 11:43:50 -0300 Subject: [PATCH 4/4] feature: README for the translate_helper.py --- utils/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 utils/README.md diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 000000000..2e6f8e1c7 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,18 @@ +# Utility scripts + +> collection of utility scripts for various tasks + +## Translate Helper + +> A script to help with translations by reading the target language, comparing with the base language, and going through missing keys. + +### Usage + +```bash +python translate_helper.py pt_BR [--check] +``` + +- `pt_BR` is the target language code (change as needed) +- `--check` is an optional flag to only check for missing keys without prompting for translations + +The script will read the base language file (`en_US.axaml`) and the target language file (e.g., `pt_BR.axaml`), identify missing keys, and prompt you to provide translations for those keys. If the `--check` flag is used, it will only list the missing keys without prompting for translations.