29
This utility offers several commands to interact with the FreeCAD project on
30
crowdin. For it to work, you need a ~/.crowdin-freecad-token file in your
31
user's folder, that contains the API access token that gives access to the
32
crowdin FreeCAD project. The API token can also be specified in the
33
CROWDIN_TOKEN environment variable.
35
The CROWDIN_PROJECT_ID environment variable can be used to use this script
40
updatecrowdin.py <command> [<arguments>]
44
gather: update all ts files found in the source code
46
status: prints a status of the translations
47
update / upload: updates crowdin the current version of .ts files
48
found in the source code
49
build: builds a new downloadable package on crowdin with all
51
build-status: shows the status of the current builds available on
53
download [build_id]: downloads build specified by 'build_id' or latest if
54
build_id is left blank
55
apply / install: applies downloaded translations to source code
56
(runs updatefromcrowdin.py)
60
./updatecrowdin.py update
62
Setting the project name adhoc:
64
CROWDIN_PROJECT_ID=some_project ./updatecrowdin.py update
69
import concurrent.futures
79
from collections import namedtuple
80
from functools import lru_cache
81
from os.path import basename, splitext
82
from urllib.parse import quote_plus
83
from urllib.request import Request
84
from urllib.request import urlopen
85
from urllib.request import urlretrieve
88
from PySide6 import QtCore
90
from PySide2 import QtCore
92
TsFile = namedtuple("TsFile", ["filename", "src_path"])
94
LEGACY_NAMING_MAP = {"Draft.ts": "draft.ts"}
113
"../Mod/AddonManager/Resources/translations",
114
"../Mod/AddonManager/Resources/AddonManager.qrc",
116
["App", "../App/Resources/translations", "../App/Resources/App.qrc"],
117
["Arch", "../Mod/BIM/Resources/translations", "../Mod/BIM/Resources/Arch.qrc"],
120
"../Mod/Assembly/Gui/Resources/translations",
121
"../Mod/Assembly/Gui/Resources/Assembly.qrc",
125
"../Mod/Draft/Resources/translations",
126
"../Mod/Draft/Resources/Draft.qrc",
128
["Base", "../Base/Resources/translations", "../Base/Resources/Base.qrc"],
131
"../Mod/Drawing/Gui/Resources/translations",
132
"../Mod/Drawing/Gui/Resources/Drawing.qrc",
136
"../Mod/Fem/Gui/Resources/translations",
137
"../Mod/Fem/Gui/Resources/Fem.qrc",
139
["FreeCAD", "../Gui/Language", "../Gui/Language/translation.qrc"],
140
["Help", "../Mod/Help/Resources/translations", "../Mod/Help/Resources/Help.qrc"],
143
"../Mod/Inspection/Gui/Resources/translations",
144
"../Mod/Inspection/Gui/Resources/Inspection.qrc",
148
"../Mod/Material/Gui/Resources/translations",
149
"../Mod/Material/Gui/Resources/Material.qrc",
153
"../Mod/Mesh/Gui/Resources/translations",
154
"../Mod/Mesh/Gui/Resources/Mesh.qrc",
158
"../Mod/MeshPart/Gui/Resources/translations",
159
"../Mod/MeshPart/Gui/Resources/MeshPart.qrc",
163
"../Mod/OpenSCAD/Resources/translations",
164
"../Mod/OpenSCAD/Resources/OpenSCAD.qrc",
168
"../Mod/Part/Gui/Resources/translations",
169
"../Mod/Part/Gui/Resources/Part.qrc",
173
"../Mod/PartDesign/Gui/Resources/translations",
174
"../Mod/PartDesign/Gui/Resources/PartDesign.qrc",
178
"../Mod/CAM/Gui/Resources/translations",
179
"../Mod/CAM/Gui/Resources/CAM.qrc",
183
"../Mod/Points/Gui/Resources/translations",
184
"../Mod/Points/Gui/Resources/Points.qrc",
187
"ReverseEngineering",
188
"../Mod/ReverseEngineering/Gui/Resources/translations",
189
"../Mod/ReverseEngineering/Gui/Resources/ReverseEngineering.qrc",
193
"../Mod/Robot/Gui/Resources/translations",
194
"../Mod/Robot/Gui/Resources/Robot.qrc",
198
"../Mod/Sketcher/Gui/Resources/translations",
199
"../Mod/Sketcher/Gui/Resources/Sketcher.qrc",
203
"../Mod/Spreadsheet/Gui/Resources/translations",
204
"../Mod/Spreadsheet/Gui/Resources/Spreadsheet.qrc",
208
"../Mod/Start/Gui/Resources/translations",
209
"../Mod/Start/Gui/Resources/Start.qrc",
213
"../Mod/Test/Gui/Resources/translations",
214
"../Mod/Test/Gui/Resources/Test.qrc",
218
"../Mod/TechDraw/Gui/Resources/translations",
219
"../Mod/TechDraw/Gui/Resources/TechDraw.qrc",
221
["Tux", "../Mod/Tux/Resources/translations", "../Mod/Tux/Resources/Tux.qrc"],
229
BASE_URL = "https://api.crowdin.com/api/v2"
231
def __init__(self, token, project_identifier, multithread=True):
233
self.project_identifier = project_identifier
234
self.multithread = multithread
237
def _get_project_id(self):
238
url = f"{self.BASE_URL}/projects/"
239
response = self._make_api_req(url)
241
for project in [p["data"] for p in response]:
242
if project["identifier"] == project_identifier:
245
raise Exception("No project identifier found!")
247
def _make_project_api_req(self, project_path, *args, **kwargs):
248
url = f"{self.BASE_URL}/projects/{self._get_project_id()}{project_path}"
249
return self._make_api_req(url=url, *args, **kwargs)
251
def _make_api_req(self, url, extra_headers={}, method="GET", data=None):
252
headers = {"Authorization": "Bearer " + load_token(), **extra_headers}
254
if type(data) is dict:
255
headers["Content-Type"] = "application/json"
256
data = json.dumps(data).encode("utf-8")
258
request = Request(url, headers=headers, method=method, data=data)
259
return json.loads(urlopen(request).read())["data"]
261
def _get_files_info(self):
262
files = self._make_project_api_req("/files?limit=250")
263
return {f["data"]["path"].strip("/"): str(f["data"]["id"]) for f in files}
265
def _add_storage(self, filename, fp):
266
response = self._make_api_req(
267
f"{self.BASE_URL}/storages",
271
"Crowdin-API-FileName": filename,
272
"Content-Type": "application/octet-stream",
275
return response["id"]
277
def _update_file(self, project_id, ts_file, files_info):
278
filename = quote_plus(ts_file.filename)
280
with open(ts_file.src_path, "rb") as fp:
281
storage_id = self._add_storage(filename, fp)
283
if filename in files_info:
284
file_id = files_info[filename]
285
self._make_project_api_req(
289
"storageId": storage_id,
290
"updateOption": "keep_translations_and_approvals",
293
print(f"{filename} updated")
295
self._make_project_api_req("/files", data={"storageId": storage_id, "name": filename})
296
print(f"{filename} uploaded")
299
response = self._make_project_api_req("/languages/progress?limit=100")
300
return [item["data"] for item in response]
302
def download(self, build_id):
303
filename = f"{self.project_identifier}.zip"
304
response = self._make_project_api_req(f"/translations/builds/{build_id}/download")
305
urlretrieve(response["url"], filename)
306
print("download of " + filename + " complete")
309
self._make_project_api_req("/translations/builds", data={}, method="POST")
311
def build_status(self):
312
response = self._make_project_api_req("/translations/builds")
313
return [item["data"] for item in response]
315
def update(self, ts_files):
316
files_info = self._get_files_info()
319
with concurrent.futures.ThreadPoolExecutor() as executor:
320
for ts_file in ts_files:
322
future = executor.submit(
323
self._update_file, self.project_identifier, ts_file, files_info
325
futures.append(future)
327
self._update_file(self.project_identifier, ts_file, files_info)
330
for future in futures:
336
config_file = os.path.expanduser("~") + os.sep + ".crowdin-freecad-token"
337
if os.path.exists(config_file):
338
with open(config_file) as file:
339
return file.read().strip()
343
def updateqrc(qrcpath, lncode):
345
"updates a qrc file with the given translation entry"
350
if not os.path.exists(qrcpath):
351
print("ERROR: Resource file " + qrcpath + " doesn't exist")
353
f = open(qrcpath, "r")
355
for l in f.readlines():
360
name = "_" + lncode + ".qm"
368
for i in range(len(resources)):
369
if ".qm" in resources[i]:
372
print("No existing .qm file in this resource. Appending to the end position")
373
for i in range(len(resources)):
374
if "</qresource>" in resources[i]:
377
print("ERROR: couldn't add qm files to this resource: " + qrcpath)
381
line = resources[pos]
383
line = re.sub(r"_.*\.qm", "_" + lncode + ".qm", line)
385
modname = os.path.splitext(os.path.basename(qrcpath))[0]
386
line = " <file>translations/" + modname + "_" + lncode + ".qm</file>\n"
390
resources.insert(pos + 1, line)
393
f = open(qrcpath, "w")
397
print("successfully updated ", qrcpath)
400
def updateTranslatorCpp(lncode):
402
"updates the Translator.cpp file with the given translation entry"
404
cppfile = os.path.join(os.path.dirname(__file__), "..", "Gui", "Language", "Translator.cpp")
405
l = QtCore.QLocale(lncode)
406
lnname = QtCore.QLocale.languageToString(l.language())
409
f = open(cppfile, "r")
411
for l in f.readlines():
417
for i, l in enumerate(cppcode):
418
if l.startswith(" d->mapLanguageTopLevelDomain[QT_TR_NOOP("):
420
if '"' + lncode + '"' in l:
427
print("ERROR: couldn't update Translator.cpp")
431
line = ' d->mapLanguageTopLevelDomain[QT_TR_NOOP("' + lnname + '")] = "' + lncode + '";\n'
432
cppcode.insert(pos, line)
433
print(lnname + " (" + lncode + ") added Translator.cpp")
436
f = open(cppfile, "w")
442
def doFile(tsfilepath, targetpath, lncode, qrcpath):
444
"updates a single ts file, and creates a corresponding qm file"
446
basename = os.path.basename(tsfilepath)[:-3]
448
if basename + ".ts" in LEGACY_NAMING_MAP.values():
449
basename = list(LEGACY_NAMING_MAP)[
450
list(LEGACY_NAMING_MAP.values()).index(basename + ".ts")
452
newname = basename + "_" + lncode + ".ts"
453
newpath = targetpath + os.sep + newname
454
if not os.path.exists(tsfilepath):
457
shutil.copyfile(tsfilepath, newpath)
458
if basename in GENERATE_QM:
468
except Exception as e:
470
newqm = targetpath + os.sep + basename + "_" + lncode + ".qm"
471
if not os.path.exists(newqm):
472
print("ERROR: failed to create " + newqm + ", aborting")
474
updateqrc(qrcpath, lncode)
477
def doLanguage(lncode):
479
"treats a single language"
486
if os.name == "posix":
489
print("Updating files for " + prefix + lncode + suffix + "...", end="")
490
for target in locations:
491
basefilepath = os.path.join(tempfolder, lncode, target[0] + ".ts")
492
targetpath = os.path.abspath(target[1])
493
qrcpath = os.path.abspath(target[2])
494
doFile(basefilepath, targetpath, lncode, qrcpath)
498
def applyTranslations(languages):
501
currentfolder = os.getcwd()
502
tempfolder = tempfile.mkdtemp()
503
print("creating temp folder " + tempfolder)
504
src = os.path.join(currentfolder, "freecad.zip")
505
dst = os.path.join(tempfolder, "freecad.zip")
506
if not os.path.exists(src):
507
print('freecad.zip file not found! Aborting. Run "download" command before this one.')
509
shutil.copyfile(src, dst)
511
zfile = zipfile.ZipFile("freecad.zip")
512
print("extracting freecad.zip...")
514
os.chdir(currentfolder)
516
if not os.path.exists(os.path.join(tempfolder, ln)):
517
print("ERROR: language path for " + ln + " not found!")
522
if __name__ == "__main__":
529
token = os.environ.get("CROWDIN_TOKEN", load_token())
530
if command and not token:
531
print("Token not found")
534
project_identifier = os.environ.get("CROWDIN_PROJECT_ID")
535
if not project_identifier:
536
project_identifier = "freecad"
540
updater = CrowdinUpdater(token, project_identifier)
542
if command == "status":
543
status = updater.status()
544
status = sorted(status, key=lambda item: item["translationProgress"], reverse=True)
546
len([item for item in status if item["translationProgress"] > THRESHOLD]),
547
" languages with status > " + str(THRESHOLD) + "%:",
553
if os.name == "posix":
557
if item["translationProgress"] > 0:
558
if (item["translationProgress"] < THRESHOLD) and (not sep):
560
print("Other languages:")
568
+ str(item["translationProgress"])
570
+ str(item["approvalProgress"])
576
elif command == "build-status":
577
for item in updater.build_status():
578
print(f" id: {item['id']} progress: {item['progress']}% status: {item['status']}")
580
elif command == "build":
583
elif command == "download":
585
updater.download(args[1])
587
stat = updater.build_status()
589
print("no builds found")
591
updater.download(stat[0]["id"])
593
print("available builds:")
596
f" id: {item['id']} progress: {item['progress']}% status: {item['status']}"
598
print("please specify a build id")
600
elif command in ["update", "upload"]:
602
all_ts_files = glob.glob("../**/*.ts", recursive=True)
604
ts_files_wo_ext = [splitext(f)[0] for f in all_ts_files]
606
main_ts_files = list(
608
lambda f: not [a for a in ts_files_wo_ext if a in f and f != a],
613
names_and_path = [(f"{basename(f)}.ts", f"{f}.ts") for f in main_ts_files]
616
TsFile(LEGACY_NAMING_MAP[a] if a in LEGACY_NAMING_MAP else a, b)
617
for (a, b) in names_and_path
619
updater.update(ts_files)
621
elif command in ["apply", "install"]:
622
print("retrieving list of languages...")
623
status = updater.status()
624
status = sorted(status, key=lambda item: item["translationProgress"], reverse=True)
626
item["languageId"] for item in status if item["translationProgress"] > THRESHOLD
628
applyTranslations(languages)
629
print("Updating Translator.cpp...")
631
updateTranslatorCpp(ln)
633
elif command == "updateTranslator":
634
print("retrieving list of languages...")
635
status = updater.status()
636
status = sorted(status, key=lambda item: item["translationProgress"], reverse=True)
638
item["languageId"] for item in status if item["translationProgress"] > THRESHOLD
640
print("Updating Translator.cpp...")
642
updateTranslatorCpp(ln)
644
elif command == "gather":