OpenShot Video Editor  2.0.0
generate_translations.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file updates the OpenShot.POT (language translation template) by scanning all source files.
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # This file helps you generate the POT file that contains all of the translatable
8 # strings / text in OpenShot. Because some of our text is in custom XML files,
9 # the xgettext command can't correctly generate the POT file. Thus... the
10 # existence of this file. =)
11 #
12 # Command to create the individual language PO files (Ascii files)
13 # $ msginit --input=OpenShot.pot --locale=fr_FR
14 # $ msginit --input=OpenShot.pot --locale=es
15 #
16 # Command to update the PO files (if text is added or changed)
17 # $ msgmerge en_US.po OpenShot.pot -U
18 # $ msgmerge es.po OpenShot.pot -U
19 #
20 # Command to compile the Ascii PO files into binary MO files
21 # $ msgfmt en_US.po --output-file=en_US/LC_MESSAGES/OpenShot.mo
22 # $ msgfmt es.po --output-file=es/LC_MESSAGES/OpenShot.mo
23 #
24 # Command to compile all PO files in a folder
25 # $ find -iname "*.po" -exec msgfmt {} -o {}.mo \;
26 #
27 # Command to combine the 2 pot files into 1 file
28 # $ msgcat ~/openshot/locale/OpenShot/OpenShot_source.pot ~/openshot/openshot/locale/OpenShot/OpenShot_glade.pot -o ~/openshot/main/locale/OpenShot/OpenShot.pot
29 #
30 # @section LICENSE
31 #
32 # Copyright (c) 2008-2018 OpenShot Studios, LLC
33 # (http://www.openshotstudios.com). This file is part of
34 # OpenShot Video Editor (http://www.openshot.org), an open-source project
35 # dedicated to delivering high quality video editing and animation solutions
36 # to the world.
37 #
38 # OpenShot Video Editor is free software: you can redistribute it and/or modify
39 # it under the terms of the GNU General Public License as published by
40 # the Free Software Foundation, either version 3 of the License, or
41 # (at your option) any later version.
42 #
43 # OpenShot Video Editor is distributed in the hope that it will be useful,
44 # but WITHOUT ANY WARRANTY; without even the implied warranty of
45 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46 # GNU General Public License for more details.
47 #
48 # You should have received a copy of the GNU General Public License
49 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
50 #
51 
52 import shutil
53 import datetime
54 import os
55 import subprocess
56 import sys
57 import xml.dom.minidom as xml
58 import json
59 import openshot
60 
61 # Get the absolute path of this project
62 path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
63 if path not in sys.path:
64  sys.path.append(path)
65 
66 import classes.info as info
67 from classes.logger import log
68 
69 # get the path of the main OpenShot folder
70 langage_folder_path = os.path.dirname(os.path.abspath(__file__))
71 openshot_path = os.path.dirname(langage_folder_path)
72 effects_path = os.path.join(openshot_path, 'effects')
73 blender_path = os.path.join(openshot_path, 'blender')
74 transitions_path = os.path.join(openshot_path, 'transitions')
75 titles_path = os.path.join(openshot_path, 'titles')
76 export_path = os.path.join(openshot_path, 'presets')
77 windows_ui_path = os.path.join(openshot_path, 'windows', 'ui')
78 locale_path = os.path.join(openshot_path, 'locale', 'OpenShot')
79 
80 log.info("-----------------------------------------------------")
81 log.info(" Creating temp POT files")
82 log.info("-----------------------------------------------------")
83 
84 # create empty temp files in the /openshot/language folder (these are used as 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(langage_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(langage_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(langage_folder_path, 'OpenShot_QtUi.ts')), shell=True)
109 subprocess.call('lupdate *.ui -ts %s' % (os.path.join(langage_folder_path, 'OpenShot_QtUi.pot')), shell=True)
110 os.chdir(langage_folder_path)
111 
112 # Rewrite the UI POT, removing msgctxt
113 output = open(os.path.join(langage_folder_path, "clean.po"), 'w')
114 for line in open(os.path.join(langage_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(langage_folder_path, "clean.po"), os.path.join(langage_folder_path, 'OpenShot_QtUi.pot'))
120 os.remove(os.path.join(langage_folder_path, "clean.po"))
121 
122 # Remove duplicates (if any found)
123 subprocess.call('msguniq %s --use-first -o %s' % (os.path.join(langage_folder_path, 'OpenShot_QtUi.pot'),
124  os.path.join(langage_folder_path, 'clean.po')), shell=True)
125 shutil.copy(os.path.join(langage_folder_path, "clean.po"), os.path.join(langage_folder_path, 'OpenShot_QtUi.pot'))
126 os.remove(os.path.join(langage_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(langage_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(langage_folder_path, temp_file)):
146  os.remove(os.path.join(langage_folder_path, temp_file))
147  f = open(os.path.join(langage_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-2016 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>, 2016.\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 (this removes dupes) ")
352 log.info("-----------------------------------------------------")
353 
354 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot', 'OpenShot_effects.pot', 'OpenShot_export.pot',
355  'OpenShot_transitions.pot', 'OpenShot_QtUi.pot']
356 command = "msgcat"
357 for temp_file in temp_files:
358  # append files
359  command = command + " " + os.path.join(langage_folder_path, temp_file)
360 command = command + " -o " + os.path.join(locale_path, "OpenShot.pot")
361 
362 log.info(command)
363 
364 # merge all 4 temp POT files
365 subprocess.call(command, shell=True)
366 
367 log.info("-----------------------------------------------------")
368 log.info(" Create FINAL POT File from all temp POT files ")
369 log.info("-----------------------------------------------------")
370 
371 # get the entire text of OpenShot.POT
372 f = open(os.path.join(locale_path, "OpenShot.pot"), "r")
373 # read entire text of file
374 entire_source = f.read()
375 f.close()
376 
377 # Create Final POT Output File
378 if os.path.exists(os.path.join(locale_path, "OpenShot.pot")):
379  os.remove(os.path.join(locale_path, "OpenShot.pot"))
380 final = open(os.path.join(locale_path, "OpenShot.pot"), "w")
381 final.write(header_text)
382 final.write("\n")
383 
384 # Trim the beginning off of each POT file
385 start_pos = entire_source.find("#: ")
386 trimmed_source = entire_source[start_pos:]
387 
388 # Add to Final POT File
389 final.write(trimmed_source)
390 final.write("\n")
391 
392 # Close final POT file
393 final.close()
394 
395 log.info("-----------------------------------------------------")
396 log.info(" Remove all temp POT files ")
397 log.info("-----------------------------------------------------")
398 
399 # Delete all 4 temp files
400 temp_files = ['OpenShot_source.pot', 'OpenShot_glade.pot', 'OpenShot_effects.pot', 'OpenShot_export.pot',
401  'OpenShot_transitions.pot', 'OpenShot_QtUi.pot', 'OpenShot_QtUi.ts']
402 for temp_file_name in temp_files:
403  temp_file_path = os.path.join(langage_folder_path, temp_file_name)
404  if os.path.exists(temp_file_path):
405  os.remove(temp_file_path)
406 
407 # output success
408 log.info("-----------------------------------------------------")
409 log.info(" The /openshot/locale/OpenShot/OpenShot.pot file has")
410 log.info(" been successfully created with all text in OpenShot.")
411 log.info("-----------------------------------------------------")