-
Notifications
You must be signed in to change notification settings - Fork 1
/
mtt_convert.py
418 lines (379 loc) · 15 KB
/
mtt_convert.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to convert Minetest *.tr files to *.po and vice-versa.
#
# Copyright (C) 2023 Wuzzy
# License: LGPLv2.1 or later (see LICENSE file for details)
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
from sys import stderr as _stderr
# Name of directory to export *.po files into
DIRNAME = "poconvert"
SCRIPTNAME = "mtt_convert"
VERSION = "0.1.0"
MODE_PO2TR = 0
MODE_TR2PO = 1
# comment to mark the section of old/unused strings
comment_unused = "##### not used anymore #####"
# Running params
params = {"recursive": False,
"help": False,
"verbose": False,
"po2tr": False,
"tr2po": False,
"folders": [],
}
# Available CLI options
options = {
"po2tr": ['--po2tr', '-P'],
"tr2po": ['--tr2po', '-T'],
"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"verbose": ['--verbose', '-v'],
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 80
pattern_tr = re.compile(r'(.*?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_tr_language_code = re.compile(r'.*\.([a-zA-Z]+)\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["po2tr"])}
convert from *.po to *.tr files
{', '.join(options["tr2po"])}
convert from *.tr to *.po files
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["verbose"])}
add output information''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
else:
mode = None
if params["po2tr"] and not params["tr2po"]:
mode = MODE_PO2TR
elif params["tr2po"] and not params["po2tr"]:
mode = MODE_TR2PO
else:
print("You must select a conversion mode (--po2tr or --tr2po)")
exit(1)
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(mode, f)
else:
update_folder(mode, f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(mode, params["folders"][0])
else:
update_folder(mode, params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(mode, os.path.abspath("./"))
else:
update_folder(mode, os.path.abspath("./"))
#attempt to read the mod's name from the mod.conf file or folder name. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
if not os.path.isfile(os.path.join(folder, "modpack.txt")):
folder_name = os.path.basename(folder)
# Special case when run in Minetest's builtin directory
if folder_name == "builtin":
return "__builtin"
else:
return folder_name
else:
return None
return None
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
if params["verbose"]:
print(f"Processing PO file ...")
# escape '@' signs except those followed by digit 1-9
text = re.sub(r'(@)(?![1-9])', "@@", text)
# escape equals signs
text = re.sub(r'=', "@=", text)
# The first three items are for unused matches
text = re.sub(r'^#~ msgid "', "", text, flags=re.MULTILINE)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# clear comment lines
text = re.sub(r'^#.*\n', "", text, flags=re.MULTILINE)
# converting msg pairs into "=" pairs
text = re.sub(r'^msgid "', "", text, flags=re.MULTILINE)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove leading whitespace and double-spaced lines
text = text.lstrip()
oldtext = ''
while text != oldtext:
oldtext = text
text = re.sub(r'\n\n', '\n', text)
return text
def generate_po_header(textdomain, language):
if textdomain == "__builtin":
project_id = "Minetest builtin component"
else:
project_id = "Minetest textdomain " + textdomain
# fake version number
project_version = "x.x.x"
project_id_version = project_id + " " + project_version
header = """msgid ""
msgstr ""
"Project-Id-Version: """+project_id_version+"""\\n"
"Report-Msgid-Bugs-To: \\n"
"POT-Creation-Date: \\n"
"PO-Revision-Date: \\n"
"Last-Translator: \\n"
"Language-Team: \\n"
"Language: """ + language + """\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: \\n"
"X-Generator: """+SCRIPTNAME+" "+VERSION+"""\\n"
"""
return header
def escape_for_tr(text):
# Temporarily replace " and @@ with ASCII ESC char + another character
# so they don't conflict with the *.tr escape codes
text = re.sub(r'"', "\033q", text)
text = re.sub(r'@@', "\033d", text)
# unescape *.tr special chars
text = re.sub(r'@n', '\\\\n\"\n\"', text)
text = re.sub(r'@=', "=", text)
# Undo the ASCII escapes
# Restore \033d to @, not @@ because that's another *.tr escape
text = re.sub("\033d", "@", text)
text = re.sub("\033q", '\\"', text)
return text
# Convert .tr to .po or .pot
# If language is the empty string, will create a template
def process_tr_file(text, textdomain, language):
if params["verbose"]:
print(f"Processing TR file ... (textdomain={textdomain}; language={language})")
stext = ""
# write header
stext = generate_po_header(textdomain, language) + stext
# ignore everything after the special line marking unused strings
unusedMatch = re.search("\n" + comment_unused, text)
if (unusedMatch != None):
text = text[0:unusedMatch.start()]
# match strings and write in PO-style
strings = re.findall(r'^(.*(?<!@))=(.*)$', text, flags=re.MULTILINE)
for s in strings:
source = s[0]
source = escape_for_tr(source)
# Is language is empty string, caller wants a template,
# so translation is left empty
translation = ""
if language != "":
translation = s[1]
translation = escape_for_tr(translation)
stext = stext + 'msgid \"' + source + '\"\n'
stext = stext + 'msgstr \"' + translation + '\"\n'
stext = stext + '\n'
return stext
# Go through existing .tr files and, if a .po file for that language
# *doesn't* exist, convert it and create it.
def process_tr_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale')):
for name in files:
language_code = None
if name == 'template.txt':
language_code = ""
else:
code_match = pattern_tr_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
po_name = None
if language_code == None:
continue
elif language_code != "":
po_name = f'{language_code}.po'
else:
po_name = "template.pot"
mkdir_p(os.path.join(root, DIRNAME))
po_file = os.path.join(root, DIRNAME, po_name)
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as tr_file:
if params["verbose"]:
print(f"Importing translations from {name}")
# Convert file contents to *.po syntax
text = process_tr_file(tr_file.read(), modname, language_code)
with open(po_file, "wt", encoding='utf-8') as po_out:
po_out.write(text)
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = f'{modname}.{language_code}.tr'
tr_file = os.path.join(folder, 'locale', tr_name)
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
# Convert file contents to *.tr syntax
text = process_po_file(po_file.read())
# Add textdomain at top
text = f'# textdomain: {modname}' + '\n' + text
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
# Returns also header comments in the third return value.
def import_tr_file(tr_file):
dOut = {}
text = None
header_comment = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line.startswith("###"):
if header_comment is None and not latest_comment_block is None:
# Save header comments
header_comment = latest_comment_block
# Strip textdomain line
tmp_h_c = ""
for l in header_comment.split('\n'):
if not l.startswith("# textdomain:"):
tmp_h_c += l + '\n'
header_comment = tmp_h_c
# Reset comment block if we hit a header
latest_comment_block = None
continue
elif line.startswith("#"):
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text, header_comment)
# Updates translation files for the mod in the given folder
def update_mod(mode, folder):
modname = get_modname(folder)
if modname is not None:
if mode == MODE_TR2PO:
print(f"Converting TR files for {modname}")
process_tr_files(folder, modname)
elif mode == MODE_PO2TR:
print(f"Converting PO files for {modname}")
process_po_files(folder, modname)
else:
print("ERROR: Invalid mode provided in update_mod()")
exit(1)
else:
print(f"\033[31mUnable to find modname in folder {folder}.\033[0m", file=_stderr)
exit(1)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(mode, folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]
for subfolder in subfolders:
update_mod(mode, subfolder)
else:
update_mod(mode, folder)
print("Done.")
def run_all_subfolders(mode, folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]:
update_folder(mode, modfolder)
main()