Merge branch 'blender-v2.92-release'
[blender-addons.git] / ui_translate / update_addon.py
blob38b0ac8a63a8dd28d28dd94a86452e2d2a4ff149
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.types import Operator
29 from bpy.props import (
30 BoolProperty,
31 EnumProperty,
32 StringProperty,
34 from . import settings
35 from bl_i18n_utils import utils as utils_i18n
36 from bl_i18n_utils import bl_extract_messages
38 from bpy.app.translations import pgettext_iface as iface_
39 import addon_utils
41 import io
42 import os
43 import shutil
44 import subprocess
45 import tempfile
48 # Helpers ###################################################################
50 def validate_module(op, context):
51 module_name = op.module_name
52 addon = getattr(context, "active_addon", None)
53 if addon:
54 module_name = addon.module
56 if not module_name:
57 op.report({'ERROR'}, "No add-on module given!")
58 return None, None
60 mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
61 if not mod:
62 op.report({'ERROR'}, "Add-on '{}' not found!".format(module_name))
63 return None, None
64 return module_name, mod[0]
67 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
68 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
69 _cached_enum_addons = []
70 def enum_addons(self, context):
71 global _cached_enum_addons
72 setts = getattr(self, "settings", settings.settings)
73 if not _cached_enum_addons:
74 for mod in addon_utils.modules(addon_utils.addons_fake_modules):
75 mod_info = addon_utils.module_bl_info(mod)
76 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
77 if mod_info["support"] in {'OFFICIAL'}:
78 continue
79 src = mod.__file__
80 if src.endswith("__init__.py"):
81 src = os.path.dirname(src)
82 has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
83 name = mod_info["name"]
84 if has_translation:
85 name = name + " *"
86 _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
87 _cached_enum_addons.sort(key=lambda i: i[1])
88 return _cached_enum_addons
91 # Operators ###################################################################
93 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
94 class UI_OT_i18n_addon_translation_invoke(Operator):
95 """Wrapper operator which will invoke given op after setting its module_name"""
96 bl_idname = "ui.i18n_addon_translation_invoke"
97 bl_label = "Update I18n Add-on"
98 bl_property = "module_name"
100 # Operator Arguments
101 module_name: EnumProperty(
102 name="Add-on",
103 description="Add-on to process",
104 items=enum_addons,
105 options=set(),
107 op_id: StringProperty(
108 name="Operator Name",
109 description="Name (id) of the operator to invoke",
111 # /End Operator Arguments
113 def invoke(self, context, event):
114 global _cached_enum_addons
115 _cached_enum_addons[:] = []
116 context.window_manager.invoke_search_popup(self)
117 return {'RUNNING_MODAL'}
119 def execute(self, context):
120 global _cached_enum_addons
121 _cached_enum_addons[:] = []
122 if not self.op_id:
123 return {'CANCELLED'}
124 op = bpy.ops
125 for item in self.op_id.split('.'):
126 op = getattr(op, item, None)
127 if op is None:
128 return {'CANCELLED'}
129 return op('INVOKE_DEFAULT', module_name=self.module_name)
132 class UI_OT_i18n_addon_translation_update(Operator):
133 """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
134 bl_idname = "ui.i18n_addon_translation_update"
135 bl_label = "Update I18n Add-on"
137 # Operator Arguments
138 module_name: EnumProperty(
139 name="Add-on",
140 description="Add-on to process",
141 items=enum_addons,
142 options=set()
144 # /End Operator Arguments
146 def execute(self, context):
147 global _cached_enum_addons
148 _cached_enum_addons[:] = []
149 if not hasattr(self, "settings"):
150 self.settings = settings.settings
151 i18n_sett = context.window_manager.i18n_update_svn_settings
153 module_name, mod = validate_module(self, context)
155 # Generate addon-specific messages (no need for another blender instance here, this should not have any
156 # influence over the final result).
157 pot = bl_extract_messages.dump_addon_messages(module_name, True, self.settings)
159 # Now (try to) get current i18n data from the addon...
160 path = mod.__file__
161 if path.endswith("__init__.py"):
162 path = os.path.dirname(path)
164 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
166 uids = set()
167 for lng in i18n_sett.langs:
168 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
169 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
170 continue
171 if not lng.use:
172 print("Skipping {} language ({}).".format(lng.name, lng.uid))
173 continue
174 uids.add(lng.uid)
175 # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
176 uids |= (trans.trans.keys() - {lng.uid for lng in i18n_sett.langs} -
177 {self.settings.PARSER_TEMPLATE_ID, self.settings.PARSER_PY_ID})
179 # And merge!
180 for uid in uids:
181 if uid not in trans.trans:
182 trans.trans[uid] = utils_i18n.I18nMessages(uid=uid, settings=self.settings)
183 trans.trans[uid].update(pot, keep_old_commented=False)
184 trans.trans[self.settings.PARSER_TEMPLATE_ID] = pot
186 # For now we write all languages found in this trans!
187 trans.write(kind='PY')
189 return {'FINISHED'}
192 class UI_OT_i18n_addon_translation_import(Operator):
193 """Import given add-on's translation data from PO files"""
194 bl_idname = "ui.i18n_addon_translation_import"
195 bl_label = "I18n Add-on Import"
197 # Operator Arguments
198 module_name: EnumProperty(
199 name="Add-on",
200 description="Add-on to process", options=set(),
201 items=enum_addons,
204 directory: StringProperty(
205 subtype='FILE_PATH', maxlen=1024,
206 options={'HIDDEN', 'SKIP_SAVE'}
208 # /End Operator Arguments
210 def _dst(self, trans, path, uid, kind):
211 if kind == 'PO':
212 if uid == self.settings.PARSER_TEMPLATE_ID:
213 return os.path.join(self.directory, "blender.pot")
214 path = os.path.join(self.directory, uid)
215 if os.path.isdir(path):
216 return os.path.join(path, uid + ".po")
217 return path + ".po"
218 elif kind == 'PY':
219 return trans._dst(trans, path, uid, kind)
220 return path
222 def invoke(self, context, event):
223 global _cached_enum_addons
224 _cached_enum_addons[:] = []
225 if not hasattr(self, "settings"):
226 self.settings = settings.settings
227 module_name, mod = validate_module(self, context)
228 if mod:
229 self.directory = os.path.dirname(mod.__file__)
230 self.module_name = module_name
231 context.window_manager.fileselect_add(self)
232 return {'RUNNING_MODAL'}
234 def execute(self, context):
235 global _cached_enum_addons
236 _cached_enum_addons[:] = []
237 if not hasattr(self, "settings"):
238 self.settings = settings.settings
239 i18n_sett = context.window_manager.i18n_update_svn_settings
241 module_name, mod = validate_module(self, context)
242 if not (module_name and mod):
243 return {'CANCELLED'}
245 path = mod.__file__
246 if path.endswith("__init__.py"):
247 path = os.path.dirname(path)
249 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
251 # Now search given dir, to find po's matching given languages...
252 # Mapping po_uid: po_file.
253 po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
255 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
256 # file). So we just try to find the best match in po's for each enabled uid.
257 for lng in i18n_sett.langs:
258 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
259 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
260 continue
261 if not lng.use:
262 print("Skipping {} language ({}).".format(lng.name, lng.uid))
263 continue
264 uid = lng.uid
265 po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
266 if not po_uid:
267 print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
268 continue
269 po_uid = po_uid[0]
270 msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
271 if uid in trans.trans:
272 trans.trans[uid].merge(msgs, replace=True)
273 else:
274 trans.trans[uid] = msgs
276 trans.write(kind='PY')
278 return {'FINISHED'}
281 class UI_OT_i18n_addon_translation_export(Operator):
282 """Export given add-on's translation data as PO files"""
284 bl_idname = "ui.i18n_addon_translation_export"
285 bl_label = "I18n Add-on Export"
287 # Operator Arguments
288 module_name: EnumProperty(
289 name="Add-on",
290 description="Add-on to process",
291 items=enum_addons,
292 options=set()
295 use_export_pot: BoolProperty(
296 name="Export POT",
297 description="Export (generate) a POT file too",
298 default=True,
301 use_update_existing: BoolProperty(
302 name="Update Existing",
303 description="Update existing po files, if any, instead of overwriting them",
304 default=True,
307 directory: StringProperty(
308 subtype='FILE_PATH', maxlen=1024,
309 options={'HIDDEN', 'SKIP_SAVE'}
311 # /End Operator Arguments
313 def _dst(self, trans, path, uid, kind):
314 if kind == 'PO':
315 if uid == self.settings.PARSER_TEMPLATE_ID:
316 return os.path.join(self.directory, "blender.pot")
317 path = os.path.join(self.directory, uid)
318 if os.path.isdir(path):
319 return os.path.join(path, uid + ".po")
320 return path + ".po"
321 elif kind == 'PY':
322 return trans._dst(trans, path, uid, kind)
323 return path
325 def invoke(self, context, event):
326 global _cached_enum_addons
327 _cached_enum_addons[:] = []
328 if not hasattr(self, "settings"):
329 self.settings = settings.settings
330 module_name, mod = validate_module(self, context)
331 if mod:
332 self.directory = os.path.dirname(mod.__file__)
333 self.module_name = module_name
334 context.window_manager.fileselect_add(self)
335 return {'RUNNING_MODAL'}
337 def execute(self, context):
338 global _cached_enum_addons
339 _cached_enum_addons[:] = []
340 if not hasattr(self, "settings"):
341 self.settings = settings.settings
342 i18n_sett = context.window_manager.i18n_update_svn_settings
344 module_name, mod = validate_module(self, context)
345 if not (module_name and mod):
346 return {'CANCELLED'}
348 path = mod.__file__
349 if path.endswith("__init__.py"):
350 path = os.path.dirname(path)
352 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
353 trans.dst = self._dst
355 uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
356 for lng in i18n_sett.langs:
357 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
358 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
359 continue
360 if not lng.use:
361 print("Skipping {} language ({}).".format(lng.name, lng.uid))
362 continue
363 uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
364 if uid:
365 uids.append(uid[0])
367 # Try to update existing POs instead of overwriting them, if asked to do so!
368 if self.use_update_existing:
369 for uid in uids:
370 if uid == self.settings.PARSER_TEMPLATE_ID:
371 continue
372 path = trans.dst(trans, trans.src[uid], uid, 'PO')
373 if not os.path.isfile(path):
374 continue
375 msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
376 msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
377 trans.msgs[uid] = msgs
379 trans.write(kind='PO', langs=set(uids))
381 return {'FINISHED'}
384 classes = (
385 UI_OT_i18n_addon_translation_invoke,
386 UI_OT_i18n_addon_translation_update,
387 UI_OT_i18n_addon_translation_import,
388 UI_OT_i18n_addon_translation_export,