remove '.' from descriptions
[blender-addons.git] / ui_translate / update_addon.py
blobcbf1999f840c88fd80e6185a60cd18e5aac99aca
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 imp
23 imp.reload(settings)
24 imp.reload(utils_i18n)
25 imp.reload(bl_extract_messages)
26 else:
27 import bpy
28 from bpy.props import (BoolProperty,
29 CollectionProperty,
30 EnumProperty,
31 FloatProperty,
32 FloatVectorProperty,
33 IntProperty,
34 PointerProperty,
35 StringProperty,
37 from . import settings
38 from bl_i18n_utils import utils as utils_i18n
39 from bl_i18n_utils import bl_extract_messages
41 from bpy.app.translations import pgettext_iface as iface_
42 import addon_utils
44 import io
45 import os
46 import shutil
47 import subprocess
48 import tempfile
51 ##### Helpers #####
52 def validate_module(op, context):
53 module_name = op.module_name
54 addon = getattr(context, "active_addon", None)
55 if addon:
56 module_name = addon.module
58 if not module_name:
59 op.report({'ERROR'}, "No addon module given!")
60 return None, None
62 mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
63 if not mod:
64 op.report({'ERROR'}, "Addon '{}' not found!".format(module_name))
65 return None, None
66 return module_name, mod[0]
69 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
70 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
71 _cached_enum_addons = []
72 def enum_addons(self, context):
73 global _cached_enum_addons
74 setts = getattr(self, "settings", settings.settings)
75 if not _cached_enum_addons:
76 for mod in addon_utils.modules(addon_utils.addons_fake_modules):
77 mod_info = addon_utils.module_bl_info(mod)
78 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
79 if mod_info["support"] in {'OFFICIAL'}:
80 continue
81 src = mod.__file__
82 if src.endswith("__init__.py"):
83 src = os.path.dirname(src)
84 has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
85 name = mod_info["name"]
86 if has_translation:
87 name = name + " *"
88 _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
89 _cached_enum_addons.sort(key=lambda i: i[1])
90 return _cached_enum_addons
93 ##### Operators #####
94 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
95 class UI_OT_i18n_addon_translation_invoke(bpy.types.Operator):
96 """Wrapper operator which will invoke given op after setting its module_name"""
97 bl_idname = "ui.i18n_addon_translation_invoke"
98 bl_label = "Update I18n Addon"
99 bl_property = "module_name"
101 module_name = EnumProperty(items=enum_addons, name="Addon", description="Addon to process", options=set())
102 op_id = StringProperty(name="Operator Name", description="Name (id) of the operator to invoke")
104 def invoke(self, context, event):
105 global _cached_enum_addons
106 _cached_enum_addons[:] = []
107 context.window_manager.invoke_search_popup(self)
108 return {'RUNNING_MODAL'}
110 def execute(self, context):
111 global _cached_enum_addons
112 _cached_enum_addons[:] = []
113 if not self.op_id:
114 return {'CANCELLED'}
115 op = bpy.ops
116 for item in self.op_id.split('.'):
117 op = getattr(op, item, None)
118 #print(self.op_id, item, op)
119 if op is None:
120 return {'CANCELLED'}
121 return op('INVOKE_DEFAULT', module_name=self.module_name)
123 class UI_OT_i18n_addon_translation_update(bpy.types.Operator):
124 """Update given addon's translation data (found as a py tuple in the addon's source code)"""
125 bl_idname = "ui.i18n_addon_translation_update"
126 bl_label = "Update I18n Addon"
128 module_name = EnumProperty(items=enum_addons, name="Addon", description="Addon to process", options=set())
130 def execute(self, context):
131 global _cached_enum_addons
132 _cached_enum_addons[:] = []
133 if not hasattr(self, "settings"):
134 self.settings = settings.settings
135 i18n_sett = context.window_manager.i18n_update_svn_settings
137 module_name, mod = validate_module(self, context)
139 # Generate addon-specific messages (no need for another blender instance here, this should not have any
140 # influence over the final result).
141 pot = bl_extract_messages.dump_addon_messages(module_name, True, self.settings)
143 # Now (try to) get current i18n data from the addon...
144 path = mod.__file__
145 if path.endswith("__init__.py"):
146 path = os.path.dirname(path)
148 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
150 uids = set()
151 for lng in i18n_sett.langs:
152 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
153 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
154 continue
155 if not lng.use:
156 print("Skipping {} language ({}).".format(lng.name, lng.uid))
157 continue
158 uids.add(lng.uid)
159 # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
160 uids |= (trans.trans.keys() - {lng.uid for lng in i18n_sett.langs} -
161 {self.settings.PARSER_TEMPLATE_ID, self.settings.PARSER_PY_ID})
163 # And merge!
164 for uid in uids:
165 if uid not in trans.trans:
166 trans.trans[uid] = utils_i18n.I18nMessages(uid=uid, settings=self.settings)
167 trans.trans[uid].update(pot, keep_old_commented=False)
168 trans.trans[self.settings.PARSER_TEMPLATE_ID] = pot
170 # For now we write all languages found in this trans!
171 trans.write(kind='PY')
173 return {'FINISHED'}
176 class UI_OT_i18n_addon_translation_import(bpy.types.Operator):
177 """Import given addon's translation data from PO files"""
178 bl_idname = "ui.i18n_addon_translation_import"
179 bl_label = "I18n Addon Import"
181 module_name = EnumProperty(items=enum_addons, name="Addon", description="Addon to process", options=set())
182 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
184 def _dst(self, trans, path, uid, kind):
185 if kind == 'PO':
186 if uid == self.settings.PARSER_TEMPLATE_ID:
187 return os.path.join(self.directory, "blender.pot")
188 path = os.path.join(self.directory, uid)
189 if os.path.isdir(path):
190 return os.path.join(path, uid + ".po")
191 return path + ".po"
192 elif kind == 'PY':
193 return trans._dst(trans, path, uid, kind)
194 return path
196 def invoke(self, context, event):
197 global _cached_enum_addons
198 _cached_enum_addons[:] = []
199 if not hasattr(self, "settings"):
200 self.settings = settings.settings
201 module_name, mod = validate_module(self, context)
202 if mod:
203 self.directory = os.path.dirname(mod.__file__)
204 self.module_name = module_name
205 context.window_manager.fileselect_add(self)
206 return {'RUNNING_MODAL'}
208 def execute(self, context):
209 global _cached_enum_addons
210 _cached_enum_addons[:] = []
211 if not hasattr(self, "settings"):
212 self.settings = settings.settings
213 i18n_sett = context.window_manager.i18n_update_svn_settings
215 module_name, mod = validate_module(self, context)
216 if not (module_name and mod):
217 return {'CANCELLED'}
219 path = mod.__file__
220 if path.endswith("__init__.py"):
221 path = os.path.dirname(path)
223 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
225 # Now search given dir, to find po's matching given languages...
226 # Mapping po_uid: po_file.
227 po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
229 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
230 # file). So we just try to find the best match in po's for each enabled uid.
231 for lng in i18n_sett.langs:
232 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
233 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
234 continue
235 if not lng.use:
236 print("Skipping {} language ({}).".format(lng.name, lng.uid))
237 continue
238 uid = lng.uid
239 po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
240 if not po_uid:
241 print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
242 continue
243 po_uid = po_uid[0]
244 msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
245 if uid in trans.trans:
246 trans.trans[uid].merge(msgs, replace=True)
247 else:
248 trans.trans[uid] = msgs
250 trans.write(kind='PY')
252 return {'FINISHED'}
255 class UI_OT_i18n_addon_translation_export(bpy.types.Operator):
256 """Export given addon's translation data as PO files"""
257 bl_idname = "ui.i18n_addon_translation_export"
258 bl_label = "I18n Addon Export"
260 module_name = EnumProperty(items=enum_addons, name="Addon", description="Addon to process", options=set())
261 use_export_pot = BoolProperty(name="Export POT", default=True, description="Export (generate) a POT file too")
262 use_update_existing = BoolProperty(name="Update Existing", default=True,
263 description="Update existing po files, if any, instead of overwriting them")
264 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
266 def _dst(self, trans, path, uid, kind):
267 if kind == 'PO':
268 if uid == self.settings.PARSER_TEMPLATE_ID:
269 return os.path.join(self.directory, "blender.pot")
270 path = os.path.join(self.directory, uid)
271 if os.path.isdir(path):
272 return os.path.join(path, uid + ".po")
273 return path + ".po"
274 elif kind == 'PY':
275 return trans._dst(trans, path, uid, kind)
276 return path
278 def invoke(self, context, event):
279 global _cached_enum_addons
280 _cached_enum_addons[:] = []
281 if not hasattr(self, "settings"):
282 self.settings = settings.settings
283 module_name, mod = validate_module(self, context)
284 if mod:
285 self.directory = os.path.dirname(mod.__file__)
286 self.module_name = module_name
287 context.window_manager.fileselect_add(self)
288 return {'RUNNING_MODAL'}
290 def execute(self, context):
291 global _cached_enum_addons
292 _cached_enum_addons[:] = []
293 if not hasattr(self, "settings"):
294 self.settings = settings.settings
295 i18n_sett = context.window_manager.i18n_update_svn_settings
297 module_name, mod = validate_module(self, context)
298 if not (module_name and mod):
299 return {'CANCELLED'}
301 path = mod.__file__
302 if path.endswith("__init__.py"):
303 path = os.path.dirname(path)
305 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
306 trans.dst = self._dst
308 uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
309 for lng in i18n_sett.langs:
310 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
311 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
312 continue
313 if not lng.use:
314 print("Skipping {} language ({}).".format(lng.name, lng.uid))
315 continue
316 uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
317 if uid:
318 uids.append(uid[0])
320 # Try to update existing POs instead of overwriting them, if asked to do so!
321 if self.use_update_existing:
322 for uid in uids:
323 if uid == self.settings.PARSER_TEMPLATE_ID:
324 continue
325 path = trans.dst(trans, trans.src[uid], uid, 'PO')
326 if not os.path.isfile(path):
327 continue
328 msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
329 msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
330 trans.msgs[uid] = msgs
332 trans.write(kind='PO', langs=set(uids))
334 return {'FINISHED'}