Node Wrangler: do not add reroutes to unavailable outputs
[blender-addons.git] / ui_translate / update_addon.py
blob6167d38f0691ba3ed536a586f1add604c13d50b5
1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 if "bpy" in locals():
6 import importlib
7 importlib.reload(settings)
8 importlib.reload(utils_i18n)
9 importlib.reload(bl_extract_messages)
10 else:
11 import bpy
12 from bpy.types import Operator
13 from bpy.props import (
14 BoolProperty,
15 EnumProperty,
16 StringProperty,
18 from . import settings
19 from bl_i18n_utils import utils as utils_i18n
20 from bl_i18n_utils import bl_extract_messages
22 from bpy.app.translations import pgettext_iface as iface_
23 import addon_utils
25 import io
26 import os
27 import shutil
28 import subprocess
29 import tempfile
32 # Helpers ###################################################################
34 def validate_module(op, context):
35 module_name = op.module_name
36 addon = getattr(context, "active_addon", None)
37 if addon:
38 module_name = addon.module
40 if not module_name:
41 op.report({'ERROR'}, "No add-on module given!")
42 return None, None
44 mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
45 if not mod:
46 op.report({'ERROR'}, "Add-on '{}' not found!".format(module_name))
47 return None, None
48 return module_name, mod[0]
51 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
52 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
53 _cached_enum_addons = []
54 def enum_addons(self, context):
55 global _cached_enum_addons
56 setts = getattr(self, "settings", settings.settings)
57 if not _cached_enum_addons:
58 for mod in addon_utils.modules(module_cache=addon_utils.addons_fake_modules):
59 mod_info = addon_utils.module_bl_info(mod)
60 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
61 if mod_info["support"] in {'OFFICIAL'}:
62 continue
63 src = mod.__file__
64 if src.endswith("__init__.py"):
65 src = os.path.dirname(src)
66 has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
67 name = mod_info["name"]
68 if has_translation:
69 name = name + " *"
70 _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
71 _cached_enum_addons.sort(key=lambda i: i[1])
72 return _cached_enum_addons
75 # Operators ###################################################################
77 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
78 class UI_OT_i18n_addon_translation_invoke(Operator):
79 """Wrapper operator which will invoke given op after setting its module_name"""
80 bl_idname = "ui.i18n_addon_translation_invoke"
81 bl_label = "Update I18n Add-on"
82 bl_property = "module_name"
84 # Operator Arguments
85 module_name: EnumProperty(
86 name="Add-on",
87 description="Add-on to process",
88 items=enum_addons,
89 options=set(),
91 op_id: StringProperty(
92 name="Operator Name",
93 description="Name (id) of the operator to invoke",
95 # /End Operator Arguments
97 def invoke(self, context, event):
98 global _cached_enum_addons
99 _cached_enum_addons[:] = []
100 context.window_manager.invoke_search_popup(self)
101 return {'RUNNING_MODAL'}
103 def execute(self, context):
104 global _cached_enum_addons
105 _cached_enum_addons[:] = []
106 if not self.op_id:
107 return {'CANCELLED'}
108 op = bpy.ops
109 for item in self.op_id.split('.'):
110 op = getattr(op, item, None)
111 if op is None:
112 return {'CANCELLED'}
113 return op('INVOKE_DEFAULT', module_name=self.module_name)
116 class UI_OT_i18n_addon_translation_update(Operator):
117 """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
118 bl_idname = "ui.i18n_addon_translation_update"
119 bl_label = "Update I18n Add-on"
121 # Operator Arguments
122 module_name: EnumProperty(
123 name="Add-on",
124 description="Add-on to process",
125 items=enum_addons,
126 options=set()
128 # /End Operator Arguments
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_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(Operator):
177 """Import given add-on's translation data from PO files"""
178 bl_idname = "ui.i18n_addon_translation_import"
179 bl_label = "I18n Add-on Import"
181 # Operator Arguments
182 module_name: EnumProperty(
183 name="Add-on",
184 description="Add-on to process", options=set(),
185 items=enum_addons,
188 directory: StringProperty(
189 subtype='FILE_PATH', maxlen=1024,
190 options={'HIDDEN', 'SKIP_SAVE'}
192 # /End Operator Arguments
194 def _dst(self, trans, path, uid, kind):
195 if kind == 'PO':
196 if uid == self.settings.PARSER_TEMPLATE_ID:
197 return os.path.join(self.directory, "blender.pot")
198 path = os.path.join(self.directory, uid)
199 if os.path.isdir(path):
200 return os.path.join(path, uid + ".po")
201 return path + ".po"
202 elif kind == 'PY':
203 return trans._dst(trans, path, uid, kind)
204 return path
206 def invoke(self, context, event):
207 global _cached_enum_addons
208 _cached_enum_addons[:] = []
209 if not hasattr(self, "settings"):
210 self.settings = settings.settings
211 module_name, mod = validate_module(self, context)
212 if mod:
213 self.directory = os.path.dirname(mod.__file__)
214 self.module_name = module_name
215 context.window_manager.fileselect_add(self)
216 return {'RUNNING_MODAL'}
218 def execute(self, context):
219 global _cached_enum_addons
220 _cached_enum_addons[:] = []
221 if not hasattr(self, "settings"):
222 self.settings = settings.settings
223 i18n_sett = context.window_manager.i18n_update_settings
225 module_name, mod = validate_module(self, context)
226 if not (module_name and mod):
227 return {'CANCELLED'}
229 path = mod.__file__
230 if path.endswith("__init__.py"):
231 path = os.path.dirname(path)
233 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
235 # Now search given dir, to find po's matching given languages...
236 # Mapping po_uid: po_file.
237 po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
239 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
240 # file). So we just try to find the best match in po's for each enabled uid.
241 for lng in i18n_sett.langs:
242 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
243 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
244 continue
245 if not lng.use:
246 print("Skipping {} language ({}).".format(lng.name, lng.uid))
247 continue
248 uid = lng.uid
249 po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
250 if not po_uid:
251 print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
252 continue
253 po_uid = po_uid[0]
254 msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
255 if uid in trans.trans:
256 trans.trans[uid].merge(msgs, replace=True)
257 else:
258 trans.trans[uid] = msgs
260 trans.write(kind='PY')
262 return {'FINISHED'}
265 class UI_OT_i18n_addon_translation_export(Operator):
266 """Export given add-on's translation data as PO files"""
268 bl_idname = "ui.i18n_addon_translation_export"
269 bl_label = "I18n Add-on Export"
271 # Operator Arguments
272 module_name: EnumProperty(
273 name="Add-on",
274 description="Add-on to process",
275 items=enum_addons,
276 options=set()
279 use_export_pot: BoolProperty(
280 name="Export POT",
281 description="Export (generate) a POT file too",
282 default=True,
285 use_update_existing: BoolProperty(
286 name="Update Existing",
287 description="Update existing po files, if any, instead of overwriting them",
288 default=True,
291 directory: StringProperty(
292 subtype='FILE_PATH', maxlen=1024,
293 options={'HIDDEN', 'SKIP_SAVE'}
295 # /End Operator Arguments
297 def _dst(self, trans, path, uid, kind):
298 if kind == 'PO':
299 if uid == self.settings.PARSER_TEMPLATE_ID:
300 return os.path.join(self.directory, "blender.pot")
301 path = os.path.join(self.directory, uid)
302 if os.path.isdir(path):
303 return os.path.join(path, uid + ".po")
304 return path + ".po"
305 elif kind == 'PY':
306 return trans._dst(trans, path, uid, kind)
307 return path
309 def invoke(self, context, event):
310 global _cached_enum_addons
311 _cached_enum_addons[:] = []
312 if not hasattr(self, "settings"):
313 self.settings = settings.settings
314 module_name, mod = validate_module(self, context)
315 if mod:
316 self.directory = os.path.dirname(mod.__file__)
317 self.module_name = module_name
318 context.window_manager.fileselect_add(self)
319 return {'RUNNING_MODAL'}
321 def execute(self, context):
322 global _cached_enum_addons
323 _cached_enum_addons[:] = []
324 if not hasattr(self, "settings"):
325 self.settings = settings.settings
326 i18n_sett = context.window_manager.i18n_update_settings
328 module_name, mod = validate_module(self, context)
329 if not (module_name and mod):
330 return {'CANCELLED'}
332 path = mod.__file__
333 if path.endswith("__init__.py"):
334 path = os.path.dirname(path)
336 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
337 trans.dst = self._dst
339 uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
340 for lng in i18n_sett.langs:
341 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
342 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
343 continue
344 if not lng.use:
345 print("Skipping {} language ({}).".format(lng.name, lng.uid))
346 continue
347 translation_keys = {k for k in trans.trans.keys()
348 if k != self.settings.PARSER_TEMPLATE_ID}
349 uid = utils_i18n.find_best_isocode_matches(lng.uid, translation_keys)
350 if uid:
351 uids.append(uid[0])
353 # Try to update existing POs instead of overwriting them, if asked to do so!
354 if self.use_update_existing:
355 for uid in uids:
356 if uid == self.settings.PARSER_TEMPLATE_ID:
357 continue
358 path = trans.dst(trans, trans.src[uid], uid, 'PO')
359 if not os.path.isfile(path):
360 continue
361 msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
362 msgs.update(trans.trans[self.settings.PARSER_TEMPLATE_ID])
363 trans.trans[uid] = msgs
365 trans.write(kind='PO', langs=set(uids))
367 return {'FINISHED'}
370 classes = (
371 UI_OT_i18n_addon_translation_invoke,
372 UI_OT_i18n_addon_translation_update,
373 UI_OT_i18n_addon_translation_import,
374 UI_OT_i18n_addon_translation_export,