OpenShot Video Editor  2.0.0
generate_translations.py
Go to the documentation of this file.
1 #!/usr/bin/python3
2 ##
3 #
4 # @file
5 # @brief This file updates the OpenShot.POT (language translation template) by scanning all source files.
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 #
8 # This file helps you generate the POT file that contains all of the translatable
9 # strings / text in OpenShot. Because some of our text is in custom XML files,
10 # the xgettext command can't correctly generate the POT file. Thus... the
11 # existence of this file. =)
12 #
13 # Command to create the individual language PO files (Ascii files)
14 # $ msginit --input=OpenShot.pot --locale=fr_FR
15 # $ msginit --input=OpenShot.pot --locale=es
16 #
17 # Command to update the PO files (if text is added or changed)
18 # $ msgmerge en_US.po OpenShot.pot -U
19 # $ msgmerge es.po OpenShot.pot -U
20 #
21 # Command to compile the Ascii PO files into binary MO files
22 # $ msgfmt en_US.po --output-file=en_US/LC_MESSAGES/OpenShot.mo
23 # $ msgfmt es.po --output-file=es/LC_MESSAGES/OpenShot.mo
24 #
25 # Command to compile all PO files in a folder
26 # $ find -iname "*.po" -exec msgfmt {} -o {}.mo \;
27 #
28 # Command to combine the 2 pot files into 1 file
29 # $ msgcat ~/openshot/locale/OpenShot/OpenShot_source.pot ~/openshot/openshot/locale/OpenShot/OpenShot_glade.pot -o ~/openshot/main/locale/OpenShot/OpenShot.pot
30 #
31 # @section LICENSE
32 #
33 # Copyright (c) 2008-2018 OpenShot Studios, LLC
34 # (http://www.openshotstudios.com). This file is part of
35 # OpenShot Video Editor (http://www.openshot.org), an open-source project
36 # dedicated to delivering high quality video editing and animation solutions
37 # to the world.
38 #
39 # OpenShot Video Editor is free software: you can redistribute it and/or modify
40 # it under the terms of the GNU General Public License as published by
41 # the Free Software Foundation, either version 3 of the License, or
42 # (at your option) any later version.
43 #
44 # OpenShot Video Editor is distributed in the hope that it will be useful,
45 # but WITHOUT ANY WARRANTY; without even the implied warranty of
46 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 # GNU General Public License for more details.
48 #
49 # You should have received a copy of the GNU General Public License
50 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
51 #
52 
53 import shutil
54 import datetime
55 import os
56 import subprocess
57 import sys
58 import xml.dom.minidom as xml
59 import json
60 import openshot
61 
62 # Get the absolute path of this project
63 path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
64 if path not in sys.path:
65  sys.path.append(path)
66 
67 import classes.info as info
68 from classes.logger import log
69 
70 # get the path of the main OpenShot folder
71 language_folder_path = os.path.dirname(os.path.abspath(__file__))
72 openshot_path = os.path.dirname(language_folder_path)
73 effects_path = os.path.join(openshot_path, 'effects')
74 blender_path = os.path.join(openshot_path, 'blender')
75 transitions_path = os.path.join(openshot_path, 'transitions')
76 titles_path = os.path.join(openshot_path, 'titles')
77 export_path = os.path.join(openshot_path, 'presets')
78 windows_ui_path = os.path.join(openshot_path, 'windows', 'ui')
79 
80 log.info("-----------------------------------------------------")
81 log.info(" Creating temp POT files")
82 log.info("-----------------------------------------------------")
83 
84 # create empty temp POT files
85 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot', 'OpenShot_effects.pot', 'OpenShot_export.pot',
86  'OpenShot_transitions.pot', 'OpenShot_QtUi.pot']
87 for temp_file_name in temp_files:
88  temp_file_path = os.path.join(language_folder_path, temp_file_name)
89  if os.path.exists(temp_file_path):
90  os.remove(temp_file_path)
91  f = open(temp_file_path, "w")
92  f.close()
93 
94 log.info("-----------------------------------------------------")
95 log.info(" Using xgettext to generate .py POT files")
96 log.info("-----------------------------------------------------")
97 
98 # Generate POT for Source Code strings (i.e. strings marked with a _("translate me"))
99 subprocess.call('find %s -iname "*.py" -exec xgettext -j -o %s --keyword=_ {} \;' % (
100 openshot_path, os.path.join(language_folder_path, 'OpenShot_source.pot')), shell=True)
101 
102 log.info("-----------------------------------------------------")
103 log.info(" Using Qt's lupdate to generate .ui POT files")
104 log.info("-----------------------------------------------------")
105 
106 # Generate POT for Qt *.ui files (which require the lupdate command, and ts2po command)
107 os.chdir(windows_ui_path)
108 subprocess.call('lupdate *.ui -ts %s' % (os.path.join(language_folder_path, 'OpenShot_QtUi.ts')), shell=True)
109 subprocess.call('lupdate *.ui -ts %s' % (os.path.join(language_folder_path, 'OpenShot_QtUi.pot')), shell=True)
110 os.chdir(language_folder_path)
111 
112 # Rewrite the UI POT, removing msgctxt
113 output = open(os.path.join(language_folder_path, "clean.po"), 'w')
114 for line in open(os.path.join(language_folder_path, 'OpenShot_QtUi.pot'), 'r'):
115  if not line.startswith('msgctxt'):
116  output.write(line)
117 # Overwrite original PO file
118 output.close()
119 shutil.copy(os.path.join(language_folder_path, "clean.po"), os.path.join(language_folder_path, 'OpenShot_QtUi.pot'))
120 os.remove(os.path.join(language_folder_path, "clean.po"))
121 
122 # Remove duplicates (if any found)
123 subprocess.call('msguniq %s --use-first -o %s' % (os.path.join(language_folder_path, 'OpenShot_QtUi.pot'),
124  os.path.join(language_folder_path, 'clean.po')), shell=True)
125 shutil.copy(os.path.join(language_folder_path, "clean.po"), os.path.join(language_folder_path, 'OpenShot_QtUi.pot'))
126 os.remove(os.path.join(language_folder_path, "clean.po"))
127 
128 
129 log.info("-----------------------------------------------------")
130 log.info(" Updating auto created POT files to set CharSet")
131 log.info("-----------------------------------------------------")
132 
133 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot']
134 for temp_file in temp_files:
135  # get the entire text
136  f = open(os.path.join(language_folder_path, temp_file), "r")
137  # read entire text of file
138  entire_source = f.read()
139  f.close()
140 
141  # replace charset
142  entire_source = entire_source.replace("charset=CHARSET", "charset=UTF-8")
143 
144  # Create Updated POT Output File
145  if os.path.exists(os.path.join(language_folder_path, temp_file)):
146  os.remove(os.path.join(language_folder_path, temp_file))
147  f = open(os.path.join(language_folder_path, temp_file), "w")
148  f.write(entire_source)
149  f.close()
150 
151 log.info("-----------------------------------------------------")
152 log.info(" Scanning custom XML files and finding text")
153 log.info("-----------------------------------------------------")
154 
155 # Loop through the Effects XML
156 effects_text = {}
157 for file in os.listdir(effects_path):
158  if os.path.isfile(os.path.join(effects_path, file)):
159  # load xml effect file
160  full_file_path = os.path.join(effects_path, file)
161  xmldoc = xml.parse(os.path.join(effects_path, file))
162 
163  # add text to list
164  effects_text[xmldoc.getElementsByTagName("title")[0].childNodes[0].data] = full_file_path
165  effects_text[xmldoc.getElementsByTagName("description")[0].childNodes[0].data] = full_file_path
166 
167  # get params
168  params = xmldoc.getElementsByTagName("param")
169 
170  # Loop through params
171  for param in params:
172  if param.attributes["title"]:
173  effects_text[param.attributes["title"].value] = full_file_path
174 
175 # Append on properties from libopenshot
176 objects = [openshot.Clip(), openshot.Bars(), openshot.Blur(), openshot.Brightness(),
177  openshot.ChromaKey(), openshot.ColorShift(), openshot.Crop(), openshot.Deinterlace(), openshot.Hue(), openshot.Mask(),
178  openshot.Negate(), openshot.Pixelate(), openshot.Saturation(), openshot.Shift(), openshot.Wave()]
179 
180 # Loop through each libopenshot object
181 for object in objects:
182  props = json.loads(object.PropertiesJSON(1))
183 
184  # Loop through props
185  for key in props.keys():
186  object = props[key]
187  if "name" in object.keys():
188  effects_text[object["name"]] = "libopenshot (Clip Properties)"
189  if "choices" in object.keys():
190  for choice in object["choices"]:
191  effects_text[choice["name"]] = "libopenshot (Clip Properties)"
192 
193 # Append Effect Meta Data
194 e = openshot.EffectInfo()
195 props = json.loads(e.Json())
196 
197 # Loop through props
198 for effect in props:
199  if "name" in effect:
200  effects_text[effect["name"]] = "libopenshot (Effect Metadata)"
201  if "description" in effect:
202  effects_text[effect["description"]] = "libopenshot (Effect Metadata)"
203 
204 # Loop through the Blender XML
205 for file in os.listdir(blender_path):
206  if os.path.isfile(os.path.join(blender_path, file)):
207  # load xml effect file
208  full_file_path = os.path.join(blender_path, file)
209  xmldoc = xml.parse(os.path.join(blender_path, file))
210 
211  # add text to list
212  effects_text[xmldoc.getElementsByTagName("title")[0].childNodes[0].data] = full_file_path
213 
214  # get params
215  params = xmldoc.getElementsByTagName("param")
216 
217  # Loop through params
218  for param in params:
219  if param.attributes["title"]:
220  effects_text[param.attributes["title"].value] = full_file_path
221 
222 # Loop through the Export Settings XML
223 export_text = {}
224 for file in os.listdir(export_path):
225  if os.path.isfile(os.path.join(export_path, file)):
226  # load xml export file
227  full_file_path = os.path.join(export_path, file)
228  xmldoc = xml.parse(os.path.join(export_path, file))
229 
230  # add text to list
231  export_text[xmldoc.getElementsByTagName("type")[0].childNodes[0].data] = full_file_path
232  export_text[xmldoc.getElementsByTagName("title")[0].childNodes[0].data] = full_file_path
233 
234 # Loop through Settings
235 settings_file = open(os.path.join(info.PATH, 'settings', '_default.settings'), 'r').read()
236 settings = json.loads(settings_file)
237 category_names = []
238 for setting in settings:
239  if "type" in setting and setting["type"] != "hidden":
240  # Add visible settings
241  export_text[setting["title"]] = "Settings for %s" % setting["setting"]
242  if "type" in setting and setting["type"] != "hidden":
243  # Add visible category names
244  if setting["category"] not in category_names:
245  export_text[setting["category"]] = "Settings Category for %s" % setting["category"]
246  category_names.append(setting["category"])
247 
248 # Loop through transitions and add to POT file
249 transitions_text = {}
250 for file in os.listdir(transitions_path):
251  # load xml export file
252  full_file_path = os.path.join(transitions_path, file)
253  (fileBaseName, fileExtension) = os.path.splitext(file)
254 
255  # get transition name
256  name = fileBaseName.replace("_", " ").capitalize()
257 
258  # add text to list
259  transitions_text[name] = full_file_path
260 
261  # Look in sub-folders
262  for sub_file in os.listdir(full_file_path):
263  # load xml export file
264  full_subfile_path = os.path.join(full_file_path, sub_file)
265  (fileBaseName, fileExtension) = os.path.splitext(sub_file)
266 
267  # split the name into parts (looking for a number)
268  suffix_number = None
269  name_parts = fileBaseName.split("_")
270  if name_parts[-1].isdigit():
271  suffix_number = name_parts[-1]
272 
273  # get transition name
274  name = fileBaseName.replace("_", " ").capitalize()
275 
276  # replace suffix number with placeholder (if any)
277  if suffix_number:
278  name = name.replace(suffix_number, "%s")
279 
280  # add text to list
281  transitions_text[name] = full_subfile_path
282 
283 # Loop through titles and add to POT file
284 for sub_file in os.listdir(titles_path):
285  # load xml export file
286  full_subfile_path = os.path.join(titles_path, sub_file)
287  (fileBaseName, fileExtension) = os.path.splitext(sub_file)
288 
289  # split the name into parts (looking for a number)
290  suffix_number = None
291  name_parts = fileBaseName.split("_")
292  if name_parts[-1].isdigit():
293  suffix_number = name_parts[-1]
294 
295  # get transition name
296  name = fileBaseName.replace("_", " ").capitalize()
297 
298  # replace suffix number with placeholder (if any)
299  if suffix_number:
300  name = name.replace(suffix_number, "%s")
301 
302  # add text to list
303  transitions_text[name] = full_subfile_path
304 
305 
306 log.info("-----------------------------------------------------")
307 log.info(" Creating the custom XML POT files")
308 log.info("-----------------------------------------------------")
309 
310 # header of POT file
311 header_text = ""
312 header_text = header_text + '# OpenShot Video Editor POT Template File.\n'
313 header_text = header_text + '# Copyright (C) 2008-2018 OpenShot Studios, LLC\n'
314 header_text = header_text + '# This file is distributed under the same license as OpenShot.\n'
315 header_text = header_text + '# Jonathan Thomas <Jonathan.Oomph@gmail.com>, 2018.\n'
316 header_text = header_text + '#\n'
317 header_text = header_text + '#, fuzzy\n'
318 header_text = header_text + 'msgid ""\n'
319 header_text = header_text + 'msgstr ""\n'
320 header_text = header_text + '"Project-Id-Version: OpenShot Video Editor (version: %s)\\n"\n' % info.VERSION
321 header_text = header_text + '"Report-Msgid-Bugs-To: Jonathan Thomas <Jonathan.Oomph@gmail.com>\\n"\n'
322 header_text = header_text + '"POT-Creation-Date: %s\\n"\n' % datetime.datetime.now()
323 header_text = header_text + '"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n'
324 header_text = header_text + '"Last-Translator: Jonathan Thomas <Jonathan.Oomph@gmail.com>\\n"\n'
325 header_text = header_text + '"Language-Team: https://translations.launchpad.net/+groups/launchpad-translators\\n"\n'
326 header_text = header_text + '"MIME-Version: 1.0\\n"\n'
327 header_text = header_text + '"Content-Type: text/plain; charset=UTF-8\\n"\n'
328 header_text = header_text + '"Content-Transfer-Encoding: 8bit\\n"\n'
329 
330 # Create POT files for the custom text (from our XML files)
331 temp_files = [['OpenShot_effects.pot', effects_text], ['OpenShot_export.pot', export_text],
332  ['OpenShot_transitions.pot', transitions_text]]
333 for temp_file, text_dict in temp_files:
334  f = open(temp_file, "w")
335 
336  # write header
337  f.write(header_text)
338 
339  # loop through each line of text
340  for k, v in text_dict.items():
341  if k:
342  f.write('\n')
343  f.write('#: %s\n' % v)
344  f.write('msgid "%s"\n' % k)
345  f.write('msgstr ""\n')
346 
347  # close file
348  f.close()
349 
350 log.info("-----------------------------------------------------")
351 log.info(" Combine all temp POT files using msgcat command ")
352 log.info(" (this removes dupes) ")
353 log.info("-----------------------------------------------------")
354 
355 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot', 'OpenShot_effects.pot', 'OpenShot_export.pot',
356  'OpenShot_transitions.pot', 'OpenShot_QtUi.pot']
357 command = "msgcat"
358 for temp_file in temp_files:
359  # append files
360  command = command + " " + os.path.join(language_folder_path, temp_file)
361 command = command + " -o " + os.path.join(language_folder_path, "OpenShot", "OpenShot.pot")
362 
363 log.info(command)
364 
365 # merge all 4 temp POT files
366 subprocess.call(command, shell=True)
367 
368 log.info("-----------------------------------------------------")
369 log.info(" Create FINAL POT File from all temp POT files ")
370 log.info("-----------------------------------------------------")
371 
372 # get the entire text of OpenShot.POT
373 f = open(os.path.join(language_folder_path, "OpenShot", "OpenShot.pot"), "r")
374 # read entire text of file
375 entire_source = f.read()
376 f.close()
377 
378 # Create Final POT Output File
379 if os.path.exists(os.path.join(language_folder_path, "OpenShot", "OpenShot.pot")):
380  os.remove(os.path.join(language_folder_path, "OpenShot", "OpenShot.pot"))
381 final = open(os.path.join(language_folder_path, "OpenShot", "OpenShot.pot"), "w")
382 final.write(header_text)
383 final.write("\n")
384 
385 # Trim the beginning off of each POT file
386 start_pos = entire_source.find("#: ")
387 trimmed_source = entire_source[start_pos:]
388 
389 # Add to Final POT File
390 final.write(trimmed_source)
391 final.write("\n")
392 
393 # Close final POT file
394 final.close()
395 
396 log.info("-----------------------------------------------------")
397 log.info(" Remove all temp POT files ")
398 log.info("-----------------------------------------------------")
399 
400 # Delete all 4 temp files
401 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot', 'OpenShot_effects.pot', 'OpenShot_export.pot',
402  'OpenShot_transitions.pot', 'OpenShot_QtUi.pot', 'OpenShot_QtUi.ts']
403 for temp_file_name in temp_files:
404  temp_file_path = os.path.join(language_folder_path, temp_file_name)
405  if os.path.exists(temp_file_path):
406  os.remove(temp_file_path)
407 
408 # output success
409 log.info("-----------------------------------------------------")
410 log.info(" The OpenShot.pot file has been successfully created ")
411 log.info(" with all text in OpenShot.")
412 log.info("-----------------------------------------------------")