Skip to content

Commit a15ca9f

Browse files
committed
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.
1 parent 494eb13 commit a15ca9f

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

utils/translate_helper.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import sys
2+
import os
3+
import xml.etree.ElementTree as ET
4+
import re
5+
6+
# Define namespaces URIs
7+
XAML_NS = 'https://github.com/avaloniaui'
8+
X_NS = 'http://schemas.microsoft.com/winfx/2006/xaml'
9+
10+
def register_namespaces():
11+
"""Registers namespaces for ElementTree to use when writing the XML file."""
12+
ET.register_namespace('', XAML_NS)
13+
ET.register_namespace('x', X_NS)
14+
15+
def get_locale_files(lang_id):
16+
"""Constructs the absolute paths for the target and reference locale files."""
17+
try:
18+
script_dir = os.path.dirname(os.path.realpath(__file__))
19+
project_root = os.path.abspath(os.path.join(script_dir, '..'))
20+
locales_dir = os.path.join(project_root, 'src', 'Resources', 'Locales')
21+
except NameError:
22+
project_root = os.path.abspath(os.getcwd())
23+
locales_dir = os.path.join(project_root, 'src', 'Resources', 'Locales')
24+
25+
target_file = os.path.join(locales_dir, f"{lang_id}.axaml")
26+
27+
if not os.path.exists(target_file):
28+
print(f"Error: Target language file not found at {target_file}")
29+
sys.exit(1)
30+
31+
try:
32+
tree = ET.parse(target_file)
33+
root = tree.getroot()
34+
merged_dict = root.find(f"{{{XAML_NS}}}ResourceDictionary.MergedDictionaries")
35+
if merged_dict is None:
36+
raise ValueError("Could not find MergedDictionaries tag.")
37+
38+
resource_include = merged_dict.find(f"{{{XAML_NS}}}ResourceInclude")
39+
if resource_include is None:
40+
raise ValueError("Could not find ResourceInclude tag.")
41+
42+
include_source = resource_include.get('Source')
43+
ref_filename_match = re.search(r'([a-zA-Z]{2}_[a-zA-Z]{2}).axaml', include_source)
44+
if not ref_filename_match:
45+
raise ValueError("Could not parse reference filename from Source attribute.")
46+
47+
ref_filename = f"{ref_filename_match.group(1)}.axaml"
48+
ref_file = os.path.join(locales_dir, ref_filename)
49+
except Exception as e:
50+
print(f"Error parsing {target_file} to find reference file: {e}")
51+
sys.exit(1)
52+
53+
if not os.path.exists(ref_file):
54+
print(f"Error: Reference language file '{ref_file}' not found.")
55+
sys.exit(1)
56+
57+
return target_file, ref_file
58+
59+
def get_strings(root):
60+
"""Extracts all translation keys and their text values from an XML root."""
61+
strings = {}
62+
for string_tag in root.findall(f"{{{X_NS}}}String"):
63+
key = string_tag.get(f"{{{X_NS}}}Key")
64+
if key:
65+
strings[key] = string_tag.text if string_tag.text is not None else ""
66+
return strings
67+
68+
def add_new_string_tag(root, key, value):
69+
"""Adds a new <x:String> tag to the XML root, maintaining some formatting."""
70+
new_tag = ET.Element(f"{{{X_NS}}}String")
71+
new_tag.set(f"{{{X_NS}}}Key", key)
72+
new_tag.set("xml:space", "preserve")
73+
new_tag.text = value
74+
75+
last_element_index = -1
76+
children = list(root)
77+
for i in range(len(children) - 1, -1, -1):
78+
if (children[i].tag == f"{{{X_NS}}}String" or
79+
children[i].tag == f"{{{XAML_NS}}}ResourceDictionary.MergedDictionaries"):
80+
last_element_index = i
81+
break
82+
83+
if last_element_index != -1:
84+
new_tag.tail = root[last_element_index].tail
85+
root.insert(last_element_index + 1, new_tag)
86+
else:
87+
new_tag.tail = "\n "
88+
root.append(new_tag)
89+
90+
def save_translations(tree, file_path):
91+
"""Saves the XML tree to the file, attempting to preserve formatting."""
92+
try:
93+
ET.indent(tree, space=" ")
94+
except AttributeError:
95+
print("Warning: ET.indent not available. Output formatting may not be ideal.")
96+
97+
tree.write(file_path, encoding='utf-8', xml_declaration=True)
98+
print(f"\nSaved changes to {file_path}")
99+
100+
def main():
101+
"""Main function to run the translation helper script."""
102+
register_namespaces()
103+
104+
if len(sys.argv) < 2:
105+
print("Usage: python utils/translate_helper.py <lang_id> [--check]")
106+
sys.exit(1)
107+
108+
lang_id = sys.argv[1]
109+
is_check_mode = len(sys.argv) > 2 and sys.argv[2] == '--check'
110+
111+
target_file_path, ref_file_path = get_locale_files(lang_id)
112+
113+
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
114+
target_tree = ET.parse(target_file_path, parser)
115+
target_root = target_tree.getroot()
116+
117+
ref_tree = ET.parse(ref_file_path)
118+
ref_root = ref_tree.getroot()
119+
120+
target_strings = get_strings(target_root)
121+
ref_strings = get_strings(ref_root)
122+
123+
missing_keys = sorted([key for key in ref_strings.keys() if key not in target_strings])
124+
125+
if not missing_keys:
126+
print("All keys are translated. Nothing to do.")
127+
return
128+
129+
print(f"Found {len(missing_keys)} missing keys for language '{lang_id}'.")
130+
131+
if is_check_mode:
132+
print("Missing keys:")
133+
for key in missing_keys:
134+
print(f" - {key}")
135+
return
136+
137+
print("Starting interactive translation...\n")
138+
changes_made = False
139+
try:
140+
for i, key in enumerate(missing_keys):
141+
original_text = ref_strings.get(key, "")
142+
print("-" * 40)
143+
print(f"({i+1}/{len(missing_keys)}) Key: '{key}'")
144+
print(f"Original: '{original_text}'")
145+
146+
user_input = input("Enter translation (or press Enter to skip, 'q' to save and quit): ")
147+
148+
if user_input.lower() == 'q':
149+
print("\nQuitting and saving changes...")
150+
break
151+
elif user_input:
152+
add_new_string_tag(target_root, key, user_input)
153+
changes_made = True
154+
print(f"Added translation for '{key}'")
155+
156+
except (KeyboardInterrupt, EOFError):
157+
print("\n\nProcess interrupted. Saving changes...")
158+
finally:
159+
if changes_made:
160+
save_translations(target_tree, target_file_path)
161+
else:
162+
print("\nNo changes were made.")
163+
164+
if __name__ == "__main__":
165+
main()

0 commit comments

Comments
 (0)