1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 importlib
.reload(settings
)
8 importlib
.reload(utils_i18n
)
9 importlib
.reload(bl_extract_messages
)
12 from bpy
.types
import Operator
13 from bpy
.props
import (
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_
32 # Helpers ###################################################################
34 def validate_module(op
, context
):
35 module_name
= op
.module_name
36 addon
= getattr(context
, "active_addon", None)
38 module_name
= addon
.module
41 op
.report({'ERROR'}, "No add-on module given!")
44 mod
= utils_i18n
.enable_addons(addons
={module_name}
, check_only
=True)
46 op
.report({'ERROR'}, "Add-on '{}' not found!".format(module_name
))
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'}:
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"]
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"
85 module_name
: EnumProperty(
87 description
="Add-on to process",
91 op_id
: StringProperty(
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
[:] = []
109 for item
in self
.op_id
.split('.'):
110 op
= getattr(op
, item
, None)
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"
122 module_name
: EnumProperty(
124 description
="Add-on to process",
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
, False, self
.settings
)
143 # Now (try to) get current i18n data from the addon...
145 if path
.endswith("__init__.py"):
146 path
= os
.path
.dirname(path
)
148 trans
= utils_i18n
.I18n(kind
='PY', src
=path
, settings
=self
.settings
)
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
))
156 print("Skipping {} language ({}).".format(lng
.name
, 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
})
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')
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"
182 module_name
: EnumProperty(
184 description
="Add-on to process", options
=set(),
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
):
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")
203 return trans
._dst
(trans
, path
, uid
, kind
)
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
)
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
):
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
))
246 print("Skipping {} language ({}).".format(lng
.name
, lng
.uid
))
249 po_uid
= utils_i18n
.find_best_isocode_matches(uid
, po_files
.keys())
251 print("Skipping {} language, no PO file found for it ({}).".format(lng
.name
, uid
))
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)
258 trans
.trans
[uid
] = msgs
260 trans
.write(kind
='PY')
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"
272 module_name
: EnumProperty(
274 description
="Add-on to process",
279 use_export_pot
: BoolProperty(
281 description
="Export (generate) a POT file too",
285 use_update_existing
: BoolProperty(
286 name
="Update Existing",
287 description
="Update existing po files, if any, instead of overwriting them",
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
):
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")
306 return trans
._dst
(trans
, path
, uid
, kind
)
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
)
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
):
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
))
345 print("Skipping {} language ({}).".format(lng
.name
, lng
.uid
))
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
)
353 # Try to update existing POs instead of overwriting them, if asked to do so!
354 if self
.use_update_existing
:
356 if uid
== self
.settings
.PARSER_TEMPLATE_ID
:
358 path
= trans
.dst(trans
, trans
.src
[uid
], uid
, 'PO')
359 if not os
.path
.isfile(path
):
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
))
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
,