Cleanup: pep8
[blender-addons.git] / ui_translate / update_addon.py
blobbac3ac45c34f3527141c350102419c44687dcdbf
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 if "bpy" in locals():
22 import importlib
23 importlib.reload(settings)
24 importlib.reload(utils_i18n)
25 importlib.reload(bl_extract_messages)
26 else:
27 import bpy
28 from bpy.props import (
29 BoolProperty,
30 CollectionProperty,
31 EnumProperty,
32 FloatProperty,
33 FloatVectorProperty,
34 IntProperty,
35 PointerProperty,
36 StringProperty,
38 from . import settings
39 from bl_i18n_utils import utils as utils_i18n
40 from bl_i18n_utils import bl_extract_messages
42 from bpy.app.translations import pgettext_iface as iface_
43 import addon_utils
45 import io
46 import os
47 import shutil
48 import subprocess
49 import tempfile
52 ##### Helpers #####
53 def validate_module(op, context):
54 module_name = op.module_name
55 addon = getattr(context, "active_addon", None)
56 if addon:
57 module_name = addon.module
59 if not module_name:
60 op.report({'ERROR'}, "No add-on module given!")
61 return None, None
63 mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
64 if not mod:
65 op.report({'ERROR'}, "Add-on '{}' not found!".format(module_name))
66 return None, None
67 return module_name, mod[0]
70 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
71 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
72 _cached_enum_addons = []
73 def enum_addons(self, context):
74 global _cached_enum_addons
75 setts = getattr(self, "settings", settings.settings)
76 if not _cached_enum_addons:
77 for mod in addon_utils.modules(addon_utils.addons_fake_modules):
78 mod_info = addon_utils.module_bl_info(mod)
79 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
80 if mod_info["support"] in {'OFFICIAL'}:
81 continue
82 src = mod.__file__
83 if src.endswith("__init__.py"):
84 src = os.path.dirname(src)
85 has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
86 name = mod_info["name"]
87 if has_translation:
88 name = name + " *"
89 _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
90 _cached_enum_addons.sort(key=lambda i: i[1])
91 return _cached_enum_addons
94 ##### Operators #####
95 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
96 class UI_OT_i18n_addon_translation_invoke(bpy.types.Operator):
97 """Wrapper operator which will invoke given op after setting its module_name"""
98 bl_idname = "ui.i18n_addon_translation_invoke"
99 bl_label = "Update I18n Add-on"
100 bl_property = "module_name"
102 module_name = EnumProperty(items=enum_addons, name="Add-on", description="Add-on to process", options=set())
103 op_id = StringProperty(name="Operator Name", description="Name (id) of the operator to invoke")
105 def invoke(self, context, event):
106 global _cached_enum_addons
107 _cached_enum_addons[:] = []
108 context.window_manager.invoke_search_popup(self)
109 return {'RUNNING_MODAL'}
111 def execute(self, context):
112 global _cached_enum_addons
113 _cached_enum_addons[:] = []
114 if not self.op_id:
115 return {'CANCELLED'}
116 op = bpy.ops
117 for item in self.op_id.split('.'):
118 op = getattr(op, item, None)
119 #print(self.op_id, item, op)
120 if op is None:
121 return {'CANCELLED'}
122 return op('INVOKE_DEFAULT', module_name=self.module_name)
124 class UI_OT_i18n_addon_translation_update(bpy.types.Operator):
125 """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
126 bl_idname = "ui.i18n_addon_translation_update"
127 bl_label = "Update I18n Add-on"
129 module_name = EnumProperty(items=enum_addons, name="Add-on", description="Add-on to process", options=set())
131 def execute(self, context):
132 global _cached_enum_addons
133 _cached_enum_addons[:] = []
134 if not hasattr(self, "settings"):
135 self.settings = settings.settings
136 i18n_sett = context.window_manager.i18n_update_svn_settings
138 module_name, mod = validate_module(self, context)
140 # Generate addon-specific messages (no need for another blender instance here, this should not have any
141 # influence over the final result).
142 pot = bl_extract_messages.dump_addon_messages(module_name, True, self.settings)
144 # Now (try to) get current i18n data from the addon...
145 path = mod.__file__
146 if path.endswith("__init__.py"):
147 path = os.path.dirname(path)
149 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
151 uids = set()
152 for lng in i18n_sett.langs:
153 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
154 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
155 continue
156 if not lng.use:
157 print("Skipping {} language ({}).".format(lng.name, lng.uid))
158 continue
159 uids.add(lng.uid)
160 # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
161 uids |= (trans.trans.keys() - {lng.uid for lng in i18n_sett.langs} -
162 {self.settings.PARSER_TEMPLATE_ID, self.settings.PARSER_PY_ID})
164 # And merge!
165 for uid in uids:
166 if uid not in trans.trans:
167 trans.trans[uid] = utils_i18n.I18nMessages(uid=uid, settings=self.settings)
168 trans.trans[uid].update(pot, keep_old_commented=False)
169 trans.trans[self.settings.PARSER_TEMPLATE_ID] = pot
171 # For now we write all languages found in this trans!
172 trans.write(kind='PY')
174 return {'FINISHED'}
177 class UI_OT_i18n_addon_translation_import(bpy.types.Operator):
178 """Import given add-on's translation data from PO files"""
179 bl_idname = "ui.i18n_addon_translation_import"
180 bl_label = "I18n Add-on Import"
182 module_name = EnumProperty(items=enum_addons, name="Add-on", description="Add-on to process", options=set())
183 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
185 def _dst(self, trans, path, uid, kind):
186 if kind == 'PO':
187 if uid == self.settings.PARSER_TEMPLATE_ID:
188 return os.path.join(self.directory, "blender.pot")
189 path = os.path.join(self.directory, uid)
190 if os.path.isdir(path):
191 return os.path.join(path, uid + ".po")
192 return path + ".po"
193 elif kind == 'PY':
194 return trans._dst(trans, path, uid, kind)
195 return path
197 def invoke(self, context, event):
198 global _cached_enum_addons
199 _cached_enum_addons[:] = []
200 if not hasattr(self, "settings"):
201 self.settings = settings.settings
202 module_name, mod = validate_module(self, context)
203 if mod:
204 self.directory = os.path.dirname(mod.__file__)
205 self.module_name = module_name
206 context.window_manager.fileselect_add(self)
207 return {'RUNNING_MODAL'}
209 def execute(self, context):
210 global _cached_enum_addons
211 _cached_enum_addons[:] = []
212 if not hasattr(self, "settings"):
213 self.settings = settings.settings
214 i18n_sett = context.window_manager.i18n_update_svn_settings
216 module_name, mod = validate_module(self, context)
217 if not (module_name and mod):
218 return {'CANCELLED'}
220 path = mod.__file__
221 if path.endswith("__init__.py"):
222 path = os.path.dirname(path)
224 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
226 # Now search given dir, to find po's matching given languages...
227 # Mapping po_uid: po_file.
228 po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
230 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
231 # file). So we just try to find the best match in po's for each enabled uid.
232 for lng in i18n_sett.langs:
233 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
234 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
235 continue
236 if not lng.use:
237 print("Skipping {} language ({}).".format(lng.name, lng.uid))
238 continue
239 uid = lng.uid
240 po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
241 if not po_uid:
242 print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
243 continue
244 po_uid = po_uid[0]
245 msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
246 if uid in trans.trans:
247 trans.trans[uid].merge(msgs, replace=True)
248 else:
249 trans.trans[uid] = msgs
251 trans.write(kind='PY')
253 return {'FINISHED'}
256 class UI_OT_i18n_addon_translation_export(bpy.types.Operator):
257 """Export given add-on's translation data as PO files"""
258 bl_idname = "ui.i18n_addon_translation_export"
259 bl_label = "I18n Add-on Export"
261 module_name = EnumProperty(items=enum_addons, name="Add-on", description="Add-on to process", options=set())
262 use_export_pot = BoolProperty(name="Export POT", default=True, description="Export (generate) a POT file too")
263 use_update_existing = BoolProperty(name="Update Existing", default=True,
264 description="Update existing po files, if any, instead of overwriting them")
265 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
267 def _dst(self, trans, path, uid, kind):
268 if kind == 'PO':
269 if uid == self.settings.PARSER_TEMPLATE_ID:
270 return os.path.join(self.directory, "blender.pot")
271 path = os.path.join(self.directory, uid)
272 if os.path.isdir(path):
273 return os.path.join(path, uid + ".po")
274 return path + ".po"
275 elif kind == 'PY':
276 return trans._dst(trans, path, uid, kind)
277 return path
279 def invoke(self, context, event):
280 global _cached_enum_addons
281 _cached_enum_addons[:] = []
282 if not hasattr(self, "settings"):
283 self.settings = settings.settings
284 module_name, mod = validate_module(self, context)
285 if mod:
286 self.directory = os.path.dirname(mod.__file__)
287 self.module_name = module_name
288 context.window_manager.fileselect_add(self)
289 return {'RUNNING_MODAL'}
291 def execute(self, context):
292 global _cached_enum_addons
293 _cached_enum_addons[:] = []
294 if not hasattr(self, "settings"):
295 self.settings = settings.settings
296 i18n_sett = context.window_manager.i18n_update_svn_settings
298 module_name, mod = validate_module(self, context)
299 if not (module_name and mod):
300 return {'CANCELLED'}
302 path = mod.__file__
303 if path.endswith("__init__.py"):
304 path = os.path.dirname(path)
306 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
307 trans.dst = self._dst
309 uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
310 for lng in i18n_sett.langs:
311 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
312 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
313 continue
314 if not lng.use:
315 print("Skipping {} language ({}).".format(lng.name, lng.uid))
316 continue
317 uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
318 if uid:
319 uids.append(uid[0])
321 # Try to update existing POs instead of overwriting them, if asked to do so!
322 if self.use_update_existing:
323 for uid in uids:
324 if uid == self.settings.PARSER_TEMPLATE_ID:
325 continue
326 path = trans.dst(trans, trans.src[uid], uid, 'PO')
327 if not os.path.isfile(path):
328 continue
329 msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
330 msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
331 trans.msgs[uid] = msgs
333 trans.write(kind='PO', langs=set(uids))
335 return {'FINISHED'}
338 classes = (
339 UI_OT_i18n_addon_translation_invoke,
340 UI_OT_i18n_addon_translation_update,
341 UI_OT_i18n_addon_translation_import,
342 UI_OT_i18n_addon_translation_export,