Commit | Line | Data |
---|---|---|
3719602c PC |
1 | #!/usr/bin/env python3 |
2 | ||
3 | """Core options text extractor | |
4 | ||
5 | The purpose of this script is to set up & provide functions for automatic generation of 'libretro_core_options_intl.h' | |
6 | from 'libretro_core_options.h' using translations from Crowdin. | |
7 | ||
8 | Both v1 and v2 structs are supported. It is, however, recommended to convert v1 files to v2 using the included | |
9 | 'v1_to_v2_converter.py'. | |
10 | ||
11 | Usage: | |
12 | python3 path/to/core_opt_translation.py "path/to/where/libretro_core_options.h & libretro_core_options_intl.h/are" | |
13 | ||
14 | This script will: | |
15 | 1.) create key words for & extract the texts from libretro_core_options.h & save them into intl/_us/core_options.h | |
16 | 2.) do the same for any present translations in libretro_core_options_intl.h, saving those in their respective folder | |
17 | """ | |
18 | import core_option_regex as cor | |
19 | import re | |
20 | import os | |
21 | import sys | |
22 | import json | |
23 | import urllib.request as req | |
24 | import shutil | |
25 | ||
26 | # for uploading translations to Crowdin, the Crowdin 'language id' is required | |
27 | LANG_CODE_TO_ID = {'_ar': 'ar', | |
28 | '_ast': 'ast', | |
29 | '_chs': 'zh-CN', | |
30 | '_cht': 'zh-TW', | |
31 | '_cs': 'cs', | |
32 | '_cy': 'cy', | |
33 | '_da': 'da', | |
34 | '_de': 'de', | |
35 | '_el': 'el', | |
36 | '_eo': 'eo', | |
37 | '_es': 'es-ES', | |
38 | '_fa': 'fa', | |
39 | '_fi': 'fi', | |
40 | '_fr': 'fr', | |
41 | '_gl': 'gl', | |
42 | '_he': 'he', | |
43 | '_hu': 'hu', | |
44 | '_id': 'id', | |
45 | '_it': 'it', | |
46 | '_ja': 'ja', | |
47 | '_ko': 'ko', | |
48 | '_nl': 'nl', | |
49 | '_pl': 'pl', | |
50 | '_pt_br': 'pt-BR', | |
51 | '_pt_pt': 'pt-PT', | |
52 | '_ru': 'ru', | |
53 | '_sk': 'sk', | |
54 | '_sv': 'sv-SE', | |
55 | '_tr': 'tr', | |
56 | '_uk': 'uk', | |
57 | '_vn': 'vi'} | |
58 | LANG_CODE_TO_R_LANG = {'_ar': 'RETRO_LANGUAGE_ARABIC', | |
59 | '_ast': 'RETRO_LANGUAGE_ASTURIAN', | |
60 | '_chs': 'RETRO_LANGUAGE_CHINESE_SIMPLIFIED', | |
61 | '_cht': 'RETRO_LANGUAGE_CHINESE_TRADITIONAL', | |
62 | '_cs': 'RETRO_LANGUAGE_CZECH', | |
63 | '_cy': 'RETRO_LANGUAGE_WELSH', | |
64 | '_da': 'RETRO_LANGUAGE_DANISH', | |
65 | '_de': 'RETRO_LANGUAGE_GERMAN', | |
66 | '_el': 'RETRO_LANGUAGE_GREEK', | |
67 | '_eo': 'RETRO_LANGUAGE_ESPERANTO', | |
68 | '_es': 'RETRO_LANGUAGE_SPANISH', | |
69 | '_fa': 'RETRO_LANGUAGE_PERSIAN', | |
70 | '_fi': 'RETRO_LANGUAGE_FINNISH', | |
71 | '_fr': 'RETRO_LANGUAGE_FRENCH', | |
72 | '_gl': 'RETRO_LANGUAGE_GALICIAN', | |
73 | '_he': 'RETRO_LANGUAGE_HEBREW', | |
74 | '_hu': 'RETRO_LANGUAGE_HUNGARIAN', | |
75 | '_id': 'RETRO_LANGUAGE_INDONESIAN', | |
76 | '_it': 'RETRO_LANGUAGE_ITALIAN', | |
77 | '_ja': 'RETRO_LANGUAGE_JAPANESE', | |
78 | '_ko': 'RETRO_LANGUAGE_KOREAN', | |
79 | '_nl': 'RETRO_LANGUAGE_DUTCH', | |
80 | '_pl': 'RETRO_LANGUAGE_POLISH', | |
81 | '_pt_br': 'RETRO_LANGUAGE_PORTUGUESE_BRAZIL', | |
82 | '_pt_pt': 'RETRO_LANGUAGE_PORTUGUESE_PORTUGAL', | |
83 | '_ru': 'RETRO_LANGUAGE_RUSSIAN', | |
84 | '_sk': 'RETRO_LANGUAGE_SLOVAK', | |
85 | '_sv': 'RETRO_LANGUAGE_SWEDISH', | |
86 | '_tr': 'RETRO_LANGUAGE_TURKISH', | |
87 | '_uk': 'RETRO_LANGUAGE_UKRAINIAN', | |
88 | '_us': 'RETRO_LANGUAGE_ENGLISH', | |
89 | '_vn': 'RETRO_LANGUAGE_VIETNAMESE'} | |
90 | ||
91 | # these are handled by RetroArch directly - no need to include them in core translations | |
92 | ON_OFFS = {'"enabled"', '"disabled"', '"true"', '"false"', '"on"', '"off"'} | |
93 | ||
94 | ||
95 | def remove_special_chars(text: str, char_set=0) -> str: | |
96 | """Removes special characters from a text. | |
97 | ||
98 | :param text: String to be cleaned. | |
99 | :param char_set: 0 -> remove all ASCII special chars except for '_' & 'space'; | |
100 | 1 -> remove invalid chars from file names | |
101 | :return: Clean text. | |
102 | """ | |
103 | command_chars = [chr(unicode) for unicode in tuple(range(0, 32)) + (127,)] | |
104 | special_chars = ([chr(unicode) for unicode in tuple(range(33, 48)) + tuple(range(58, 65)) + tuple(range(91, 95)) | |
105 | + (96,) + tuple(range(123, 127))], | |
106 | ('\\', '/', ':', '*', '?', '"', '<', '>', '|')) | |
107 | res = text | |
108 | for cm in command_chars: | |
109 | res = res.replace(cm, '_') | |
110 | for sp in special_chars[char_set]: | |
111 | res = res.replace(sp, '_') | |
112 | while res.startswith('_'): | |
113 | res = res[1:] | |
114 | while res.endswith('_'): | |
115 | res = res[:-1] | |
116 | return res | |
117 | ||
118 | ||
119 | def clean_file_name(file_name: str) -> str: | |
120 | """Removes characters which might make file_name inappropriate for files on some OS. | |
121 | ||
122 | :param file_name: File name to be cleaned. | |
123 | :return: The clean file name. | |
124 | """ | |
125 | file_name = remove_special_chars(file_name, 1) | |
126 | file_name = re.sub(r'__+', '_', file_name.replace(' ', '_')) | |
127 | return file_name | |
128 | ||
129 | ||
130 | def get_struct_type_name(decl: str) -> tuple: | |
131 | """ Returns relevant parts of the struct declaration: | |
132 | type, name of the struct and the language appendix, if present. | |
133 | :param decl: The struct declaration matched by cor.p_type_name. | |
134 | :return: Tuple, e.g.: ('retro_core_option_definition', 'option_defs_us', '_us') | |
135 | """ | |
136 | struct_match = cor.p_type_name.search(decl) | |
137 | if struct_match: | |
138 | if struct_match.group(3): | |
139 | struct_type_name = struct_match.group(1, 2, 3) | |
140 | return struct_type_name | |
141 | elif struct_match.group(4): | |
142 | struct_type_name = struct_match.group(1, 2, 4) | |
143 | return struct_type_name | |
144 | else: | |
145 | struct_type_name = struct_match.group(1, 2) | |
146 | return struct_type_name | |
147 | else: | |
148 | raise ValueError(f'No or incomplete struct declaration: {decl}!\n' | |
149 | 'Please make sure all structs are complete, including the type and name declaration.') | |
150 | ||
151 | ||
152 | def is_viable_non_dupe(text: str, comparison) -> bool: | |
153 | """text must be longer than 2 ('""'), not 'NULL' and not in comparison. | |
154 | ||
155 | :param text: String to be tested. | |
156 | :param comparison: Dictionary or set to search for text in. | |
157 | :return: bool | |
158 | """ | |
159 | return 2 < len(text) and text != 'NULL' and text not in comparison | |
160 | ||
161 | ||
162 | def is_viable_value(text: str) -> bool: | |
163 | """text must be longer than 2 ('""'), not 'NULL' and text.lower() not in | |
164 | {'"enabled"', '"disabled"', '"true"', '"false"', '"on"', '"off"'}. | |
165 | ||
166 | :param text: String to be tested. | |
167 | :return: bool | |
168 | """ | |
169 | return 2 < len(text) and text != 'NULL' and text.lower() not in ON_OFFS | |
170 | ||
171 | ||
172 | def create_non_dupe(base_name: str, opt_num: int, comparison) -> str: | |
173 | """Makes sure base_name is not in comparison, and if it is it's renamed. | |
174 | ||
175 | :param base_name: Name to check/make unique. | |
176 | :param opt_num: Number of the option base_name belongs to, used in making it unique. | |
177 | :param comparison: Dictionary or set to search for base_name in. | |
178 | :return: Unique name. | |
179 | """ | |
180 | h = base_name | |
181 | if h in comparison: | |
182 | n = 0 | |
183 | h = h + '_O' + str(opt_num) | |
184 | h_end = len(h) | |
185 | while h in comparison: | |
186 | h = h[:h_end] + '_' + str(n) | |
187 | n += 1 | |
188 | return h | |
189 | ||
190 | ||
191 | def get_texts(text: str) -> dict: | |
192 | """Extracts the strings, which are to be translated/are the translations, | |
193 | from text and creates macro names for them. | |
194 | ||
195 | :param text: The string to be parsed. | |
196 | :return: Dictionary of the form { '_<lang>': { 'macro': 'string', ... }, ... }. | |
197 | """ | |
198 | # all structs: group(0) full struct, group(1) beginning, group(2) content | |
199 | structs = cor.p_struct.finditer(text) | |
200 | hash_n_string = {} | |
201 | just_string = {} | |
202 | for struct in structs: | |
203 | struct_declaration = struct.group(1) | |
204 | struct_type_name = get_struct_type_name(struct_declaration) | |
205 | if 3 > len(struct_type_name): | |
206 | lang = '_us' | |
207 | else: | |
208 | lang = struct_type_name[2] | |
209 | if lang not in just_string: | |
210 | hash_n_string[lang] = {} | |
211 | just_string[lang] = set() | |
212 | ||
213 | is_v2 = False | |
214 | pre_name = '' | |
215 | p = cor.p_info | |
216 | if 'retro_core_option_v2_definition' == struct_type_name[0]: | |
217 | is_v2 = True | |
218 | elif 'retro_core_option_v2_category' == struct_type_name[0]: | |
219 | pre_name = 'CATEGORY_' | |
220 | p = cor.p_info_cat | |
221 | ||
222 | struct_content = struct.group(2) | |
223 | # 0: full option; 1: key; 2: description; 3: additional info; 4: key/value pairs | |
224 | struct_options = cor.p_option.finditer(struct_content) | |
225 | for opt, option in enumerate(struct_options): | |
226 | # group 1: key | |
227 | if option.group(1): | |
228 | opt_name = pre_name + option.group(1) | |
229 | # no special chars allowed in key | |
230 | opt_name = remove_special_chars(opt_name).upper().replace(' ', '_') | |
231 | else: | |
232 | raise ValueError(f'No option name (key) found in struct {struct_type_name[1]} option {opt}!') | |
233 | ||
234 | # group 2: description0 | |
235 | if option.group(2): | |
236 | desc0 = option.group(2) | |
237 | if is_viable_non_dupe(desc0, just_string[lang]): | |
238 | just_string[lang].add(desc0) | |
239 | m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_LABEL'), opt, hash_n_string[lang]) | |
240 | hash_n_string[lang][m_h] = desc0 | |
241 | else: | |
242 | raise ValueError(f'No label found in struct {struct_type_name[1]} option {option.group(1)}!') | |
243 | ||
244 | # group 3: desc1, info0, info1, category | |
245 | if option.group(3): | |
246 | infos = option.group(3) | |
247 | option_info = p.finditer(infos) | |
248 | if is_v2: | |
249 | desc1 = next(option_info).group(1) | |
250 | if is_viable_non_dupe(desc1, just_string[lang]): | |
251 | just_string[lang].add(desc1) | |
252 | m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_LABEL_CAT'), opt, hash_n_string[lang]) | |
253 | hash_n_string[lang][m_h] = desc1 | |
254 | last = None | |
255 | m_h = None | |
256 | for j, info in enumerate(option_info): | |
257 | last = info.group(1) | |
258 | if is_viable_non_dupe(last, just_string[lang]): | |
259 | just_string[lang].add(last) | |
260 | m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_INFO_{j}'), opt, | |
261 | hash_n_string[lang]) | |
262 | hash_n_string[lang][m_h] = last | |
263 | if last in just_string[lang]: # category key should not be translated | |
264 | hash_n_string[lang].pop(m_h) | |
265 | just_string[lang].remove(last) | |
266 | else: | |
267 | for j, info in enumerate(option_info): | |
268 | gr1 = info.group(1) | |
269 | if is_viable_non_dupe(gr1, just_string[lang]): | |
270 | just_string[lang].add(gr1) | |
271 | m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_INFO_{j}'), opt, | |
272 | hash_n_string[lang]) | |
273 | hash_n_string[lang][m_h] = gr1 | |
274 | else: | |
275 | raise ValueError(f'Too few arguments in struct {struct_type_name[1]} option {option.group(1)}!') | |
276 | ||
277 | # group 4: | |
278 | if option.group(4): | |
279 | for j, kv_set in enumerate(cor.p_key_value.finditer(option.group(4))): | |
280 | set_key, set_value = kv_set.group(1, 2) | |
281 | if not is_viable_value(set_value): | |
282 | if not is_viable_value(set_key): | |
283 | continue | |
284 | set_value = set_key | |
285 | # re.fullmatch(r'(?:[+-][0-9]+)+', value[1:-1]) | |
286 | if set_value not in just_string[lang] and not re.sub(r'[+-]', '', set_value[1:-1]).isdigit(): | |
287 | clean_key = set_key.encode('ascii', errors='ignore').decode('unicode-escape')[1:-1] | |
288 | clean_key = remove_special_chars(clean_key).upper().replace(' ', '_') | |
289 | m_h = create_non_dupe(re.sub(r'__+', '_', f"OPTION_VAL_{clean_key}"), opt, hash_n_string[lang]) | |
290 | hash_n_string[lang][m_h] = set_value | |
291 | just_string[lang].add(set_value) | |
292 | return hash_n_string | |
293 | ||
294 | ||
295 | def create_msg_hash(intl_dir_path: str, core_name: str, keyword_string_dict: dict) -> dict: | |
296 | """Creates '<core_name>.h' files in 'intl/_<lang>/' containing the macro name & string combinations. | |
297 | ||
298 | :param intl_dir_path: Path to the intl directory. | |
299 | :param core_name: Name of the core, used for naming the files. | |
300 | :param keyword_string_dict: Dictionary of the form { '_<lang>': { 'macro': 'string', ... }, ... }. | |
301 | :return: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.h)', ... }. | |
302 | """ | |
303 | files = {} | |
304 | for localisation in keyword_string_dict: | |
305 | path = os.path.join(intl_dir_path, localisation) # intl/_<lang> | |
306 | files[localisation] = os.path.join(path, core_name + '.h') # intl/_<lang>/<core_name>.h | |
307 | if not os.path.exists(path): | |
308 | os.makedirs(path) | |
309 | with open(files[localisation], 'w', encoding='utf-8') as crowdin_file: | |
310 | out_text = '' | |
311 | for keyword in keyword_string_dict[localisation]: | |
312 | out_text = f'{out_text}{keyword} {keyword_string_dict[localisation][keyword]}\n' | |
313 | crowdin_file.write(out_text) | |
314 | return files | |
315 | ||
316 | ||
317 | def h2json(file_paths: dict) -> dict: | |
318 | """Converts .h files pointed to by file_paths into .jsons. | |
319 | ||
320 | :param file_paths: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.h)', ... }. | |
321 | :return: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.json)', ... }. | |
322 | """ | |
323 | jsons = {} | |
324 | for file_lang in file_paths: | |
325 | jsons[file_lang] = file_paths[file_lang][:-2] + '.json' | |
326 | ||
327 | p = cor.p_masked | |
328 | ||
329 | with open(file_paths[file_lang], 'r+', encoding='utf-8') as h_file: | |
330 | text = h_file.read() | |
331 | result = p.finditer(text) | |
332 | messages = {} | |
333 | for msg in result: | |
334 | key, val = msg.group(1, 2) | |
335 | if key not in messages: | |
336 | if key and val: | |
337 | # unescape & remove "\n" | |
338 | messages[key] = re.sub(r'"\s*(?:(?:/\*(?:.|[\r\n])*?\*/|//.*[\r\n]+)\s*)*"', | |
339 | '\\\n', val[1:-1].replace('\\\"', '"')) | |
340 | else: | |
341 | print(f"DUPLICATE KEY in {file_paths[file_lang]}: {key}") | |
342 | with open(jsons[file_lang], 'w', encoding='utf-8') as json_file: | |
343 | json.dump(messages, json_file, indent=2) | |
344 | ||
345 | return jsons | |
346 | ||
347 | ||
348 | def json2h(intl_dir_path: str, json_file_path: str, core_name: str) -> None: | |
349 | """Converts .json file in json_file_path into an .h ready to be included in C code. | |
350 | ||
351 | :param intl_dir_path: Path to the intl directory. | |
352 | :param json_file_path: Base path of translation .json. | |
353 | :param core_name: Name of the core, required for naming the files. | |
354 | :return: None | |
355 | """ | |
356 | h_filename = os.path.join(json_file_path, core_name + '.h') | |
357 | json_filename = os.path.join(json_file_path, core_name + '.json') | |
358 | file_lang = os.path.basename(json_file_path).upper() | |
359 | ||
360 | if os.path.basename(json_file_path).lower() == '_us': | |
361 | print(' skipped') | |
362 | return | |
363 | ||
364 | p = cor.p_masked | |
365 | ||
366 | def update(s_messages, s_template, s_source_messages): | |
367 | translation = '' | |
368 | template_messages = p.finditer(s_template) | |
369 | for tp_msg in template_messages: | |
370 | old_key = tp_msg.group(1) | |
371 | if old_key in s_messages and s_messages[old_key] != s_source_messages[old_key]: | |
372 | tl_msg_val = s_messages[old_key] | |
373 | tl_msg_val = tl_msg_val.replace('"', '\\\"').replace('\n', '') # escape | |
374 | translation = ''.join((translation, '#define ', old_key, file_lang, f' "{tl_msg_val}"\n')) | |
375 | ||
376 | else: # Remove English duplicates and non-translatable strings | |
377 | translation = ''.join((translation, '#define ', old_key, file_lang, ' NULL\n')) | |
378 | return translation | |
379 | ||
380 | with open(os.path.join(intl_dir_path, '_us', core_name + '.h'), 'r', encoding='utf-8') as template_file: | |
381 | template = template_file.read() | |
382 | with open(os.path.join(intl_dir_path, '_us', core_name + '.json'), 'r+', encoding='utf-8') as source_json_file: | |
383 | source_messages = json.load(source_json_file) | |
384 | with open(json_filename, 'r+', encoding='utf-8') as json_file: | |
385 | messages = json.load(json_file) | |
386 | new_translation = update(messages, template, source_messages) | |
387 | with open(h_filename, 'w', encoding='utf-8') as h_file: | |
388 | h_file.seek(0) | |
389 | h_file.write(new_translation) | |
390 | h_file.truncate() | |
391 | return | |
392 | ||
393 | ||
394 | def get_crowdin_client(dir_path: str) -> str: | |
395 | """Makes sure the Crowdin CLI client is present. If it isn't, it is fetched & extracted. | |
396 | ||
397 | :return: The path to 'crowdin-cli.jar'. | |
398 | """ | |
399 | jar_name = 'crowdin-cli.jar' | |
400 | jar_path = os.path.join(dir_path, jar_name) | |
401 | ||
402 | if not os.path.isfile(jar_path): | |
403 | print('Downloading crowdin-cli.jar') | |
404 | crowdin_cli_file = os.path.join(dir_path, 'crowdin-cli.zip') | |
405 | crowdin_cli_url = 'https://downloads.crowdin.com/cli/v3/crowdin-cli.zip' | |
406 | req.urlretrieve(crowdin_cli_url, crowdin_cli_file) | |
407 | import zipfile | |
408 | with zipfile.ZipFile(crowdin_cli_file, 'r') as zip_ref: | |
409 | jar_dir = zip_ref.namelist()[0] | |
410 | for file in zip_ref.namelist(): | |
411 | if file.endswith(jar_name): | |
412 | jar_file = file | |
413 | break | |
414 | zip_ref.extract(jar_file) | |
415 | os.rename(jar_file, jar_path) | |
416 | os.remove(crowdin_cli_file) | |
417 | shutil.rmtree(jar_dir) | |
418 | return jar_path | |
419 | ||
420 | ||
421 | def create_intl_file(intl_file_path: str, intl_dir_path: str, text: str, core_name: str, file_path: str) -> None: | |
422 | """Creates 'libretro_core_options_intl.h' from Crowdin translations. | |
423 | ||
424 | :param intl_file_path: Path to 'libretro_core_options_intl.h' | |
425 | :param intl_dir_path: Path to the intl directory. | |
426 | :param text: Content of the 'libretro_core_options.h' being translated. | |
427 | :param core_name: Name of the core. Needed to identify the files to pull the translations from. | |
428 | :param file_path: Path to the '<core name>_us.h' file, containing the original English texts. | |
429 | :return: None | |
430 | """ | |
431 | msg_dict = {} | |
432 | lang_up = '' | |
433 | ||
434 | def replace_pair(pair_match): | |
435 | """Replaces a key-value-pair of an option with the macros corresponding to the language. | |
436 | ||
437 | :param pair_match: The re match object representing the key-value-pair block. | |
438 | :return: Replacement string. | |
439 | """ | |
440 | offset = pair_match.start(0) | |
441 | if pair_match.group(1): # key | |
442 | if pair_match.group(2) in msg_dict: # value | |
443 | val = msg_dict[pair_match.group(2)] + lang_up | |
444 | elif pair_match.group(1) in msg_dict: # use key if value not viable (e.g. NULL) | |
445 | val = msg_dict[pair_match.group(1)] + lang_up | |
446 | else: | |
447 | return pair_match.group(0) | |
448 | else: | |
449 | return pair_match.group(0) | |
450 | res = pair_match.group(0)[:pair_match.start(2) - offset] + val \ | |
451 | + pair_match.group(0)[pair_match.end(2) - offset:] | |
452 | return res | |
453 | ||
454 | def replace_info(info_match): | |
455 | """Replaces the 'additional strings' of an option with the macros corresponding to the language. | |
456 | ||
457 | :param info_match: The re match object representing the 'additional strings' block. | |
458 | :return: Replacement string. | |
459 | """ | |
460 | offset = info_match.start(0) | |
461 | if info_match.group(1) in msg_dict: | |
462 | res = info_match.group(0)[:info_match.start(1) - offset] + \ | |
463 | msg_dict[info_match.group(1)] + lang_up + \ | |
464 | info_match.group(0)[info_match.end(1) - offset:] | |
465 | return res | |
466 | else: | |
467 | return info_match.group(0) | |
468 | ||
469 | def replace_option(option_match): | |
470 | """Replaces strings within an option | |
471 | '{ "opt_key", "label", "additional strings", ..., { {"key", "value"}, ... }, ... }' | |
472 | within a struct with the macros corresponding to the language: | |
473 | '{ "opt_key", MACRO_LABEL, MACRO_STRINGS, ..., { {"key", MACRO_VALUE}, ... }, ... }' | |
474 | ||
475 | :param option_match: The re match object representing the option. | |
476 | :return: Replacement string. | |
477 | """ | |
478 | # label | |
479 | offset = option_match.start(0) | |
480 | if option_match.group(2): | |
481 | res = option_match.group(0)[:option_match.start(2) - offset] + msg_dict[option_match.group(2)] + lang_up | |
482 | else: | |
483 | return option_match.group(0) | |
484 | # additional block | |
485 | if option_match.group(3): | |
486 | res = res + option_match.group(0)[option_match.end(2) - offset:option_match.start(3) - offset] | |
487 | new_info = p.sub(replace_info, option_match.group(3)) | |
488 | res = res + new_info | |
489 | else: | |
490 | return res + option_match.group(0)[option_match.end(2) - offset:] | |
491 | # key-value-pairs | |
492 | if option_match.group(4): | |
493 | res = res + option_match.group(0)[option_match.end(3) - offset:option_match.start(4) - offset] | |
494 | new_pairs = cor.p_key_value.sub(replace_pair, option_match.group(4)) | |
495 | res = res + new_pairs + option_match.group(0)[option_match.end(4) - offset:] | |
496 | else: | |
497 | res = res + option_match.group(0)[option_match.end(3) - offset:] | |
498 | ||
499 | return res | |
500 | ||
501 | with open(file_path, 'r+', encoding='utf-8') as template: # intl/_us/<core_name>.h | |
502 | masked_msgs = cor.p_masked.finditer(template.read()) | |
503 | for msg in masked_msgs: | |
504 | msg_dict[msg.group(2)] = msg.group(1) | |
505 | ||
506 | with open(intl_file_path, 'r', encoding='utf-8') as intl: # libretro_core_options_intl.h | |
507 | in_text = intl.read() | |
508 | intl_start = re.search(re.escape('/*\n' | |
509 | ' ********************************\n' | |
510 | ' * Core Option Definitions\n' | |
511 | ' ********************************\n' | |
512 | '*/\n'), in_text) | |
513 | if intl_start: | |
514 | out_txt = in_text[:intl_start.end(0)] | |
515 | else: | |
516 | intl_start = re.search(re.escape('#ifdef __cplusplus\n' | |
517 | 'extern "C" {\n' | |
518 | '#endif\n'), in_text) | |
519 | out_txt = in_text[:intl_start.end(0)] | |
520 | ||
521 | for folder in os.listdir(intl_dir_path): # intl/_* | |
522 | if os.path.isdir(os.path.join(intl_dir_path, folder)) and folder.startswith('_')\ | |
523 | and folder != '_us' and folder != '__pycache__': | |
524 | translation_path = os.path.join(intl_dir_path, folder, core_name + '.h') # <core_name>_<lang>.h | |
525 | # all structs: group(0) full struct, group(1) beginning, group(2) content | |
526 | struct_groups = cor.p_struct.finditer(text) | |
527 | lang_up = folder.upper() | |
528 | lang_low = folder.lower() | |
529 | out_txt = out_txt + f'/* {LANG_CODE_TO_R_LANG[lang_low]} */\n\n' # /* RETRO_LANGUAGE_NAME */ | |
530 | with open(translation_path, 'r+', encoding='utf-8') as f_in: # <core name>.h | |
531 | out_txt = out_txt + f_in.read() + '\n' | |
532 | for construct in struct_groups: | |
533 | declaration = construct.group(1) | |
534 | struct_type_name = get_struct_type_name(declaration) | |
535 | if 3 > len(struct_type_name): # no language specifier | |
536 | new_decl = re.sub(re.escape(struct_type_name[1]), struct_type_name[1] + lang_low, declaration) | |
537 | else: | |
538 | new_decl = re.sub(re.escape(struct_type_name[2]), lang_low, declaration) | |
539 | if '_us' != struct_type_name[2]: | |
540 | continue | |
541 | ||
542 | p = cor.p_info | |
543 | if 'retro_core_option_v2_category' == struct_type_name[0]: | |
544 | p = cor.p_info_cat | |
545 | offset_construct = construct.start(0) | |
546 | start = construct.end(1) - offset_construct | |
547 | end = construct.start(2) - offset_construct | |
548 | out_txt = out_txt + new_decl + construct.group(0)[start:end] | |
549 | ||
550 | content = construct.group(2) | |
551 | new_content = cor.p_option.sub(replace_option, content) | |
552 | ||
553 | start = construct.end(2) - offset_construct | |
554 | out_txt = out_txt + new_content + construct.group(0)[start:] + '\n' | |
555 | ||
556 | if 'retro_core_option_v2_definition' == struct_type_name[0]: | |
557 | out_txt = out_txt + f'struct retro_core_options_v2 options{lang_low}' \ | |
558 | ' = {\n' \ | |
559 | f' option_cats{lang_low},\n' \ | |
560 | f' option_defs{lang_low}\n' \ | |
561 | '};\n\n' | |
562 | # shutil.rmtree(JOINER.join((intl_dir_path, folder))) | |
563 | ||
564 | with open(intl_file_path, 'w', encoding='utf-8') as intl: | |
565 | intl.write(out_txt + '\n#ifdef __cplusplus\n' | |
566 | '}\n#endif\n' | |
567 | '\n#endif') | |
568 | return | |
569 | ||
570 | ||
571 | # -------------------- MAIN -------------------- # | |
572 | ||
573 | if __name__ == '__main__': | |
574 | # | |
575 | try: | |
576 | if os.path.isfile(sys.argv[1]): | |
577 | _temp = os.path.dirname(sys.argv[1]) | |
578 | else: | |
579 | _temp = sys.argv[1] | |
580 | while _temp.endswith('/') or _temp.endswith('\\'): | |
581 | _temp = _temp[:-1] | |
582 | TARGET_DIR_PATH = _temp | |
583 | except IndexError: | |
584 | TARGET_DIR_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | |
585 | print("No path provided, assuming parent directory:\n" + TARGET_DIR_PATH) | |
586 | ||
587 | DIR_PATH = os.path.dirname(os.path.realpath(__file__)) | |
588 | H_FILE_PATH = os.path.join(TARGET_DIR_PATH, 'libretro_core_options.h') | |
589 | INTL_FILE_PATH = os.path.join(TARGET_DIR_PATH, 'libretro_core_options_intl.h') | |
590 | ||
591 | _core_name = 'core_options' | |
592 | try: | |
593 | print('Getting texts from libretro_core_options.h') | |
594 | with open(H_FILE_PATH, 'r+', encoding='utf-8') as _h_file: | |
595 | _main_text = _h_file.read() | |
596 | _hash_n_str = get_texts(_main_text) | |
597 | _files = create_msg_hash(DIR_PATH, _core_name, _hash_n_str) | |
598 | _source_jsons = h2json(_files) | |
599 | except Exception as e: | |
600 | print(e) | |
601 | ||
602 | print('Getting texts from libretro_core_options_intl.h') | |
603 | with open(INTL_FILE_PATH, 'r+', encoding='utf-8') as _intl_file: | |
604 | _intl_text = _intl_file.read() | |
605 | _hash_n_str_intl = get_texts(_intl_text) | |
606 | _intl_files = create_msg_hash(DIR_PATH, _core_name, _hash_n_str_intl) | |
607 | _intl_jsons = h2json(_intl_files) | |
608 | ||
609 | print('\nAll done!') |