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"\n Saved 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 ("\n Quitting 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 \n Process interrupted. Saving changes..." )
158
+ finally :
159
+ if changes_made :
160
+ save_translations (target_tree , target_file_path )
161
+ else :
162
+ print ("\n No changes were made." )
163
+
164
+ if __name__ == "__main__" :
165
+ main ()
0 commit comments