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 #####
23 importlib
.reload(settings
)
24 importlib
.reload(utils_i18n
)
25 importlib
.reload(bl_extract_messages
)
28 from bpy
.props
import (
38 from . import settings
39 from bl_i18n_utils
import utils
as utils_i18n
40 from bl_i18n_utils
import bl_extract_messages
42 from bpy
.app
.translations
import pgettext_iface
as iface_
53 def validate_module(op
, context
):
54 module_name
= op
.module_name
55 addon
= getattr(context
, "active_addon", None)
57 module_name
= addon
.module
60 op
.report({'ERROR'}, "No add-on module given!")
63 mod
= utils_i18n
.enable_addons(addons
={module_name}
, check_only
=True)
65 op
.report({'ERROR'}, "Add-on '{}' not found!".format(module_name
))
67 return module_name
, mod
[0]
70 # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
71 # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
72 _cached_enum_addons
= []
73 def enum_addons(self
, context
):
74 global _cached_enum_addons
75 setts
= getattr(self
, "settings", settings
.settings
)
76 if not _cached_enum_addons
:
77 for mod
in addon_utils
.modules(addon_utils
.addons_fake_modules
):
78 mod_info
= addon_utils
.module_bl_info(mod
)
79 # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
80 if mod_info
["support"] in {'OFFICIAL'}:
83 if src
.endswith("__init__.py"):
84 src
= os
.path
.dirname(src
)
85 has_translation
, _
= utils_i18n
.I18n
.check_py_module_has_translations(src
, setts
)
86 name
= mod_info
["name"]
89 _cached_enum_addons
.append((mod
.__name
__, name
, mod_info
["description"]))
90 _cached_enum_addons
.sort(key
=lambda i
: i
[1])
91 return _cached_enum_addons
95 # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
96 class UI_OT_i18n_addon_translation_invoke(bpy
.types
.Operator
):
97 """Wrapper operator which will invoke given op after setting its module_name"""
98 bl_idname
= "ui.i18n_addon_translation_invoke"
99 bl_label
= "Update I18n Add-on"
100 bl_property
= "module_name"
102 module_name
= EnumProperty(items
=enum_addons
, name
="Add-on", description
="Add-on to process", options
=set())
103 op_id
= StringProperty(name
="Operator Name", description
="Name (id) of the operator to invoke")
105 def invoke(self
, context
, event
):
106 global _cached_enum_addons
107 _cached_enum_addons
[:] = []
108 context
.window_manager
.invoke_search_popup(self
)
109 return {'RUNNING_MODAL'}
111 def execute(self
, context
):
112 global _cached_enum_addons
113 _cached_enum_addons
[:] = []
117 for item
in self
.op_id
.split('.'):
118 op
= getattr(op
, item
, None)
119 #print(self.op_id, item, op)
122 return op('INVOKE_DEFAULT', module_name
=self
.module_name
)
124 class UI_OT_i18n_addon_translation_update(bpy
.types
.Operator
):
125 """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
126 bl_idname
= "ui.i18n_addon_translation_update"
127 bl_label
= "Update I18n Add-on"
129 module_name
= EnumProperty(items
=enum_addons
, name
="Add-on", description
="Add-on to process", options
=set())
131 def execute(self
, context
):
132 global _cached_enum_addons
133 _cached_enum_addons
[:] = []
134 if not hasattr(self
, "settings"):
135 self
.settings
= settings
.settings
136 i18n_sett
= context
.window_manager
.i18n_update_svn_settings
138 module_name
, mod
= validate_module(self
, context
)
140 # Generate addon-specific messages (no need for another blender instance here, this should not have any
141 # influence over the final result).
142 pot
= bl_extract_messages
.dump_addon_messages(module_name
, True, self
.settings
)
144 # Now (try to) get current i18n data from the addon...
146 if path
.endswith("__init__.py"):
147 path
= os
.path
.dirname(path
)
149 trans
= utils_i18n
.I18n(kind
='PY', src
=path
, settings
=self
.settings
)
152 for lng
in i18n_sett
.langs
:
153 if lng
.uid
in self
.settings
.IMPORT_LANGUAGES_SKIP
:
154 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng
.name
, lng
.uid
))
157 print("Skipping {} language ({}).".format(lng
.name
, lng
.uid
))
160 # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
161 uids |
= (trans
.trans
.keys() - {lng
.uid
for lng
in i18n_sett
.langs
} -
162 {self
.settings
.PARSER_TEMPLATE_ID
, self
.settings
.PARSER_PY_ID
})
166 if uid
not in trans
.trans
:
167 trans
.trans
[uid
] = utils_i18n
.I18nMessages(uid
=uid
, settings
=self
.settings
)
168 trans
.trans
[uid
].update(pot
, keep_old_commented
=False)
169 trans
.trans
[self
.settings
.PARSER_TEMPLATE_ID
] = pot
171 # For now we write all languages found in this trans!
172 trans
.write(kind
='PY')
177 class UI_OT_i18n_addon_translation_import(bpy
.types
.Operator
):
178 """Import given add-on's translation data from PO files"""
179 bl_idname
= "ui.i18n_addon_translation_import"
180 bl_label
= "I18n Add-on Import"
182 module_name
= EnumProperty(items
=enum_addons
, name
="Add-on", description
="Add-on to process", options
=set())
183 directory
= StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
185 def _dst(self
, trans
, path
, uid
, kind
):
187 if uid
== self
.settings
.PARSER_TEMPLATE_ID
:
188 return os
.path
.join(self
.directory
, "blender.pot")
189 path
= os
.path
.join(self
.directory
, uid
)
190 if os
.path
.isdir(path
):
191 return os
.path
.join(path
, uid
+ ".po")
194 return trans
._dst
(trans
, path
, uid
, kind
)
197 def invoke(self
, context
, event
):
198 global _cached_enum_addons
199 _cached_enum_addons
[:] = []
200 if not hasattr(self
, "settings"):
201 self
.settings
= settings
.settings
202 module_name
, mod
= validate_module(self
, context
)
204 self
.directory
= os
.path
.dirname(mod
.__file
__)
205 self
.module_name
= module_name
206 context
.window_manager
.fileselect_add(self
)
207 return {'RUNNING_MODAL'}
209 def execute(self
, context
):
210 global _cached_enum_addons
211 _cached_enum_addons
[:] = []
212 if not hasattr(self
, "settings"):
213 self
.settings
= settings
.settings
214 i18n_sett
= context
.window_manager
.i18n_update_svn_settings
216 module_name
, mod
= validate_module(self
, context
)
217 if not (module_name
and mod
):
221 if path
.endswith("__init__.py"):
222 path
= os
.path
.dirname(path
)
224 trans
= utils_i18n
.I18n(kind
='PY', src
=path
, settings
=self
.settings
)
226 # Now search given dir, to find po's matching given languages...
227 # Mapping po_uid: po_file.
228 po_files
= dict(utils_i18n
.get_po_files_from_dir(self
.directory
))
230 # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
231 # file). So we just try to find the best match in po's for each enabled uid.
232 for lng
in i18n_sett
.langs
:
233 if lng
.uid
in self
.settings
.IMPORT_LANGUAGES_SKIP
:
234 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng
.name
, lng
.uid
))
237 print("Skipping {} language ({}).".format(lng
.name
, lng
.uid
))
240 po_uid
= utils_i18n
.find_best_isocode_matches(uid
, po_files
.keys())
242 print("Skipping {} language, no PO file found for it ({}).".format(lng
.name
, uid
))
245 msgs
= utils_i18n
.I18nMessages(uid
=uid
, kind
='PO', key
=uid
, src
=po_files
[po_uid
], settings
=self
.settings
)
246 if uid
in trans
.trans
:
247 trans
.trans
[uid
].merge(msgs
, replace
=True)
249 trans
.trans
[uid
] = msgs
251 trans
.write(kind
='PY')
256 class UI_OT_i18n_addon_translation_export(bpy
.types
.Operator
):
257 """Export given add-on's translation data as PO files"""
258 bl_idname
= "ui.i18n_addon_translation_export"
259 bl_label
= "I18n Add-on Export"
261 module_name
= EnumProperty(items
=enum_addons
, name
="Add-on", description
="Add-on to process", options
=set())
262 use_export_pot
= BoolProperty(name
="Export POT", default
=True, description
="Export (generate) a POT file too")
263 use_update_existing
= BoolProperty(name
="Update Existing", default
=True,
264 description
="Update existing po files, if any, instead of overwriting them")
265 directory
= StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
267 def _dst(self
, trans
, path
, uid
, kind
):
269 if uid
== self
.settings
.PARSER_TEMPLATE_ID
:
270 return os
.path
.join(self
.directory
, "blender.pot")
271 path
= os
.path
.join(self
.directory
, uid
)
272 if os
.path
.isdir(path
):
273 return os
.path
.join(path
, uid
+ ".po")
276 return trans
._dst
(trans
, path
, uid
, kind
)
279 def invoke(self
, context
, event
):
280 global _cached_enum_addons
281 _cached_enum_addons
[:] = []
282 if not hasattr(self
, "settings"):
283 self
.settings
= settings
.settings
284 module_name
, mod
= validate_module(self
, context
)
286 self
.directory
= os
.path
.dirname(mod
.__file
__)
287 self
.module_name
= module_name
288 context
.window_manager
.fileselect_add(self
)
289 return {'RUNNING_MODAL'}
291 def execute(self
, context
):
292 global _cached_enum_addons
293 _cached_enum_addons
[:] = []
294 if not hasattr(self
, "settings"):
295 self
.settings
= settings
.settings
296 i18n_sett
= context
.window_manager
.i18n_update_svn_settings
298 module_name
, mod
= validate_module(self
, context
)
299 if not (module_name
and mod
):
303 if path
.endswith("__init__.py"):
304 path
= os
.path
.dirname(path
)
306 trans
= utils_i18n
.I18n(kind
='PY', src
=path
, settings
=self
.settings
)
307 trans
.dst
= self
._dst
309 uids
= [self
.settings
.PARSER_TEMPLATE_ID
] if self
.use_export_pot
else []
310 for lng
in i18n_sett
.langs
:
311 if lng
.uid
in self
.settings
.IMPORT_LANGUAGES_SKIP
:
312 print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng
.name
, lng
.uid
))
315 print("Skipping {} language ({}).".format(lng
.name
, lng
.uid
))
317 uid
= utils_i18n
.find_best_isocode_matches(lng
.uid
, trans
.trans
.keys())
321 # Try to update existing POs instead of overwriting them, if asked to do so!
322 if self
.use_update_existing
:
324 if uid
== self
.settings
.PARSER_TEMPLATE_ID
:
326 path
= trans
.dst(trans
, trans
.src
[uid
], uid
, 'PO')
327 if not os
.path
.isfile(path
):
329 msgs
= utils_i18n
.I18nMessages(kind
='PO', src
=path
, settings
=self
.settings
)
330 msgs
.update(trans
.msgs
[self
.settings
.PARSER_TEMPLATE_ID
])
331 trans
.msgs
[uid
] = msgs
333 trans
.write(kind
='PO', langs
=set(uids
))
339 UI_OT_i18n_addon_translation_invoke
,
340 UI_OT_i18n_addon_translation_update
,
341 UI_OT_i18n_addon_translation_import
,
342 UI_OT_i18n_addon_translation_export
,