Cleanup: quiet float argument to in type warning
[blender-addons.git] / ui_translate / update_addon.py
bloba32a760d108c68117420e821914f6e304ba935ad
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 if "bpy" in locals():
4 import importlib
5 importlib.reload(settings)
6 importlib.reload(utils_i18n)
7 importlib.reload(bl_extract_messages)
8 else:
9 import bpy
10 from bpy.types import Operator
11 from bpy.props import (
12 BoolProperty,
13 EnumProperty,
14 StringProperty,
16 from . import settings
17 from bl_i18n_utils import utils as utils_i18n
18 from bl_i18n_utils import bl_extract_messages
20 from bpy.app.translations import pgettext_iface as iface_
21 import addon_utils
23 import io
24 import os
25 import shutil
26 import subprocess
27 import tempfile
30 # Helpers ###################################################################
32 def validate_module(op, context):
33 module_name = op.module_name
34 addon = getattr(context, "active_addon", None)
35 if addon:
36 module_name = addon.module
38 if not module_name:
39 op.report({'ERROR'}, "No add-on module given!")
40 return None, None
42 mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
43 if not mod:
44 op.report({'ERROR'}, "Add-on '{}' not found!".format(module_name))
45 return None, None
46 return module_name, mod[0]
49 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
50 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
51 _cached_enum_addons = []
52 def enum_addons(self, context):
53 global _cached_enum_addons
54 setts = getattr(self, "settings", settings.settings)
55 if not _cached_enum_addons:
56 for mod in addon_utils.modules(module_cache=addon_utils.addons_fake_modules):
57 mod_info = addon_utils.module_bl_info(mod)
58 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
59 if mod_info["support"] in {'OFFICIAL'}:
60 continue
61 src = mod.__file__
62 if src.endswith("__init__.py"):
63 src = os.path.dirname(src)
64 has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
65 name = mod_info["name"]
66 if has_translation:
67 name = name + " *"
68 _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
69 _cached_enum_addons.sort(key=lambda i: i[1])
70 return _cached_enum_addons
73 # Operators ###################################################################
75 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
76 class UI_OT_i18n_addon_translation_invoke(Operator):
77 """Wrapper operator which will invoke given op after setting its module_name"""
78 bl_idname = "ui.i18n_addon_translation_invoke"
79 bl_label = "Update I18n Add-on"
80 bl_property = "module_name"
82 # Operator Arguments
83 module_name: EnumProperty(
84 name="Add-on",
85 description="Add-on to process",
86 items=enum_addons,
87 options=set(),
89 op_id: StringProperty(
90 name="Operator Name",
91 description="Name (id) of the operator to invoke",
93 # /End Operator Arguments
95 def invoke(self, context, event):
96 global _cached_enum_addons
97 _cached_enum_addons[:] = []
98 context.window_manager.invoke_search_popup(self)
99 return {'RUNNING_MODAL'}
101 def execute(self, context):
102 global _cached_enum_addons
103 _cached_enum_addons[:] = []
104 if not self.op_id:
105 return {'CANCELLED'}
106 op = bpy.ops
107 for item in self.op_id.split('.'):
108 op = getattr(op, item, None)
109 if op is None:
110 return {'CANCELLED'}
111 return op('INVOKE_DEFAULT', module_name=self.module_name)
114 class UI_OT_i18n_addon_translation_update(Operator):
115 """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
116 bl_idname = "ui.i18n_addon_translation_update"
117 bl_label = "Update I18n Add-on"
119 # Operator Arguments
120 module_name: EnumProperty(
121 name="Add-on",
122 description="Add-on to process",
123 items=enum_addons,
124 options=set()
126 # /End Operator Arguments
128 def execute(self, context):
129 global _cached_enum_addons
130 _cached_enum_addons[:] = []
131 if not hasattr(self, "settings"):
132 self.settings = settings.settings
133 i18n_sett = context.window_manager.i18n_update_svn_settings
135 module_name, mod = validate_module(self, context)
137 # Generate addon-specific messages (no need for another blender instance here, this should not have any
138 # influence over the final result).
139 pot = bl_extract_messages.dump_addon_messages(module_name, True, self.settings)
141 # Now (try to) get current i18n data from the addon...
142 path = mod.__file__
143 if path.endswith("__init__.py"):
144 path = os.path.dirname(path)
146 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
148 uids = set()
149 for lng in i18n_sett.langs:
150 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
151 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
152 continue
153 if not lng.use:
154 print("Skipping {} language ({}).".format(lng.name, lng.uid))
155 continue
156 uids.add(lng.uid)
157 # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
158 uids |= (trans.trans.keys() - {lng.uid for lng in i18n_sett.langs} -
159 {self.settings.PARSER_TEMPLATE_ID, self.settings.PARSER_PY_ID})
161 # And merge!
162 for uid in uids:
163 if uid not in trans.trans:
164 trans.trans[uid] = utils_i18n.I18nMessages(uid=uid, settings=self.settings)
165 trans.trans[uid].update(pot, keep_old_commented=False)
166 trans.trans[self.settings.PARSER_TEMPLATE_ID] = pot
168 # For now we write all languages found in this trans!
169 trans.write(kind='PY')
171 return {'FINISHED'}
174 class UI_OT_i18n_addon_translation_import(Operator):
175 """Import given add-on's translation data from PO files"""
176 bl_idname = "ui.i18n_addon_translation_import"
177 bl_label = "I18n Add-on Import"
179 # Operator Arguments
180 module_name: EnumProperty(
181 name="Add-on",
182 description="Add-on to process", options=set(),
183 items=enum_addons,
186 directory: StringProperty(
187 subtype='FILE_PATH', maxlen=1024,
188 options={'HIDDEN', 'SKIP_SAVE'}
190 # /End Operator Arguments
192 def _dst(self, trans, path, uid, kind):
193 if kind == 'PO':
194 if uid == self.settings.PARSER_TEMPLATE_ID:
195 return os.path.join(self.directory, "blender.pot")
196 path = os.path.join(self.directory, uid)
197 if os.path.isdir(path):
198 return os.path.join(path, uid + ".po")
199 return path + ".po"
200 elif kind == 'PY':
201 return trans._dst(trans, path, uid, kind)
202 return path
204 def invoke(self, context, event):
205 global _cached_enum_addons
206 _cached_enum_addons[:] = []
207 if not hasattr(self, "settings"):
208 self.settings = settings.settings
209 module_name, mod = validate_module(self, context)
210 if mod:
211 self.directory = os.path.dirname(mod.__file__)
212 self.module_name = module_name
213 context.window_manager.fileselect_add(self)
214 return {'RUNNING_MODAL'}
216 def execute(self, context):
217 global _cached_enum_addons
218 _cached_enum_addons[:] = []
219 if not hasattr(self, "settings"):
220 self.settings = settings.settings
221 i18n_sett = context.window_manager.i18n_update_svn_settings
223 module_name, mod = validate_module(self, context)
224 if not (module_name and mod):
225 return {'CANCELLED'}
227 path = mod.__file__
228 if path.endswith("__init__.py"):
229 path = os.path.dirname(path)
231 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
233 # Now search given dir, to find po's matching given languages...
234 # Mapping po_uid: po_file.
235 po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
237 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
238 # file). So we just try to find the best match in po's for each enabled uid.
239 for lng in i18n_sett.langs:
240 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
241 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
242 continue
243 if not lng.use:
244 print("Skipping {} language ({}).".format(lng.name, lng.uid))
245 continue
246 uid = lng.uid
247 po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
248 if not po_uid:
249 print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
250 continue
251 po_uid = po_uid[0]
252 msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
253 if uid in trans.trans:
254 trans.trans[uid].merge(msgs, replace=True)
255 else:
256 trans.trans[uid] = msgs
258 trans.write(kind='PY')
260 return {'FINISHED'}
263 class UI_OT_i18n_addon_translation_export(Operator):
264 """Export given add-on's translation data as PO files"""
266 bl_idname = "ui.i18n_addon_translation_export"
267 bl_label = "I18n Add-on Export"
269 # Operator Arguments
270 module_name: EnumProperty(
271 name="Add-on",
272 description="Add-on to process",
273 items=enum_addons,
274 options=set()
277 use_export_pot: BoolProperty(
278 name="Export POT",
279 description="Export (generate) a POT file too",
280 default=True,
283 use_update_existing: BoolProperty(
284 name="Update Existing",
285 description="Update existing po files, if any, instead of overwriting them",
286 default=True,
289 directory: StringProperty(
290 subtype='FILE_PATH', maxlen=1024,
291 options={'HIDDEN', 'SKIP_SAVE'}
293 # /End Operator Arguments
295 def _dst(self, trans, path, uid, kind):
296 if kind == 'PO':
297 if uid == self.settings.PARSER_TEMPLATE_ID:
298 return os.path.join(self.directory, "blender.pot")
299 path = os.path.join(self.directory, uid)
300 if os.path.isdir(path):
301 return os.path.join(path, uid + ".po")
302 return path + ".po"
303 elif kind == 'PY':
304 return trans._dst(trans, path, uid, kind)
305 return path
307 def invoke(self, context, event):
308 global _cached_enum_addons
309 _cached_enum_addons[:] = []
310 if not hasattr(self, "settings"):
311 self.settings = settings.settings
312 module_name, mod = validate_module(self, context)
313 if mod:
314 self.directory = os.path.dirname(mod.__file__)
315 self.module_name = module_name
316 context.window_manager.fileselect_add(self)
317 return {'RUNNING_MODAL'}
319 def execute(self, context):
320 global _cached_enum_addons
321 _cached_enum_addons[:] = []
322 if not hasattr(self, "settings"):
323 self.settings = settings.settings
324 i18n_sett = context.window_manager.i18n_update_svn_settings
326 module_name, mod = validate_module(self, context)
327 if not (module_name and mod):
328 return {'CANCELLED'}
330 path = mod.__file__
331 if path.endswith("__init__.py"):
332 path = os.path.dirname(path)
334 trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
335 trans.dst = self._dst
337 uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
338 for lng in i18n_sett.langs:
339 if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
340 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
341 continue
342 if not lng.use:
343 print("Skipping {} language ({}).".format(lng.name, lng.uid))
344 continue
345 uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
346 if uid:
347 uids.append(uid[0])
349 # Try to update existing POs instead of overwriting them, if asked to do so!
350 if self.use_update_existing:
351 for uid in uids:
352 if uid == self.settings.PARSER_TEMPLATE_ID:
353 continue
354 path = trans.dst(trans, trans.src[uid], uid, 'PO')
355 if not os.path.isfile(path):
356 continue
357 msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
358 msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
359 trans.msgs[uid] = msgs
361 trans.write(kind='PO', langs=set(uids))
363 return {'FINISHED'}
366 classes = (
367 UI_OT_i18n_addon_translation_invoke,
368 UI_OT_i18n_addon_translation_update,
369 UI_OT_i18n_addon_translation_import,
370 UI_OT_i18n_addon_translation_export,