Skip to content
Snippets Groups Projects
update_addon.py 13.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    # <pep8 compliant>
    
    if "bpy" in locals():
    
        import importlib
        importlib.reload(settings)
        importlib.reload(utils_i18n)
        importlib.reload(bl_extract_messages)
    
        from bpy.types import Operator
    
        from bpy.props import (
    
            BoolProperty,
            EnumProperty,
            StringProperty,
        )
    
        from . import settings
        from bl_i18n_utils import utils as utils_i18n
        from bl_i18n_utils import bl_extract_messages
    
    from bpy.app.translations import pgettext_iface as iface_
    import addon_utils
    
    import io
    import os
    import shutil
    import subprocess
    import tempfile
    
    
    
    # Helpers ###################################################################
    
    
    def validate_module(op, context):
        module_name = op.module_name
        addon = getattr(context, "active_addon", None)
        if addon:
            module_name = addon.module
    
        if not module_name:
    
            op.report({'ERROR'}, "No add-on module given!")
    
            return None, None
    
        mod = utils_i18n.enable_addons(addons={module_name}, check_only=True)
        if not mod:
    
            op.report({'ERROR'}, "Add-on '{}' not found!".format(module_name))
    
    # As it's a bit time heavy, I'd like to cache that enum, but this does not seem easy to do! :/
    # That "self" is not the same thing as the "self" that operators get in their invoke/execute/etc. funcs... :(
    
        setts = getattr(self, "settings", settings.settings)
    
        if not _cached_enum_addons:
            for mod in addon_utils.modules(addon_utils.addons_fake_modules):
                mod_info = addon_utils.module_bl_info(mod)
                # Skip OFFICIAL addons, they are already translated in main i18n system (together with Blender itself).
                if mod_info["support"] in {'OFFICIAL'}:
                    continue
                src = mod.__file__
                if src.endswith("__init__.py"):
                    src = os.path.dirname(src)
                has_translation, _ = utils_i18n.I18n.check_py_module_has_translations(src, setts)
                name = mod_info["name"]
                if has_translation:
                    name = name + " *"
                _cached_enum_addons.append((mod.__name__, name, mod_info["description"]))
            _cached_enum_addons.sort(key=lambda i: i[1])
        return _cached_enum_addons
    
    # Operators ###################################################################
    
    
    # This one is a helper one, as we sometimes need another invoke function (like e.g. file selection)...
    
    class UI_OT_i18n_addon_translation_invoke(Operator):
    
        """Wrapper operator which will invoke given op after setting its module_name"""
        bl_idname = "ui.i18n_addon_translation_invoke"
    
        bl_label = "Update I18n Add-on"
    
        # Operator Arguments
        module_name: EnumProperty(
            name="Add-on",
            description="Add-on to process",
            items=enum_addons,
            options=set(),
        )
        op_id: StringProperty(
            name="Operator Name",
            description="Name (id) of the operator to invoke",
        )
        # /End Operator Arguments
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            context.window_manager.invoke_search_popup(self)
            return {'RUNNING_MODAL'}
    
        def execute(self, context):
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
                return {'CANCELLED'}
    
            op = bpy.ops
            for item in self.op_id.split('.'):
                op = getattr(op, item, None)
                if op is None:
                    return {'CANCELLED'}
    
            return op('INVOKE_DEFAULT', module_name=self.module_name)
    
    
    class UI_OT_i18n_addon_translation_update(Operator):
    
        """Update given add-on's translation data (found as a py tuple in the add-on's source code)"""
    
        bl_idname = "ui.i18n_addon_translation_update"
    
        bl_label = "Update I18n Add-on"
    
        # Operator Arguments
        module_name: EnumProperty(
            name="Add-on",
            description="Add-on to process",
            items=enum_addons,
            options=set()
        )
        # /End Operator Arguments
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            if not hasattr(self, "settings"):
                self.settings = settings.settings
            i18n_sett = context.window_manager.i18n_update_svn_settings
    
            module_name, mod = validate_module(self, context)
    
            # Generate addon-specific messages (no need for another blender instance here, this should not have any
            # influence over the final result).
            pot = bl_extract_messages.dump_addon_messages(module_name, True, self.settings)
    
    
            # Now (try to) get current i18n data from the addon...
    
            path = mod.__file__
            if path.endswith("__init__.py"):
                path = os.path.dirname(path)
    
            trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
    
            uids = set()
            for lng in i18n_sett.langs:
                if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
                    print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
                    continue
                if not lng.use:
                    print("Skipping {} language ({}).".format(lng.name, lng.uid))
                    continue
                uids.add(lng.uid)
            # For now, add to processed uids all those not found in "official" list, minus "tech" ones.
            uids |= (trans.trans.keys() - {lng.uid for lng in i18n_sett.langs} -
                                          {self.settings.PARSER_TEMPLATE_ID, self.settings.PARSER_PY_ID})
    
            # And merge!
            for uid in uids:
    
                if uid not in trans.trans:
                    trans.trans[uid] = utils_i18n.I18nMessages(uid=uid, settings=self.settings)
                trans.trans[uid].update(pot, keep_old_commented=False)
    
            trans.trans[self.settings.PARSER_TEMPLATE_ID] = pot
    
            # For now we write all languages found in this trans!
            trans.write(kind='PY')
    
            return {'FINISHED'}
    
    
    
    class UI_OT_i18n_addon_translation_import(Operator):
    
        """Import given add-on's translation data from PO files"""
    
        bl_label = "I18n Add-on Import"
    
        # Operator Arguments
        module_name: EnumProperty(
            name="Add-on",
            description="Add-on to process", options=set(),
            items=enum_addons,
        )
    
        directory: StringProperty(
            subtype='FILE_PATH', maxlen=1024,
            options={'HIDDEN', 'SKIP_SAVE'}
        )
        # /End Operator Arguments
    
    
        def _dst(self, trans, path, uid, kind):
            if kind == 'PO':
                if uid == self.settings.PARSER_TEMPLATE_ID:
                    return os.path.join(self.directory, "blender.pot")
                path = os.path.join(self.directory, uid)
                if os.path.isdir(path):
                    return os.path.join(path, uid + ".po")
                return path + ".po"
            elif kind == 'PY':
                return trans._dst(trans, path, uid, kind)
            return path
    
        def invoke(self, context, event):
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            if not hasattr(self, "settings"):
                self.settings = settings.settings
            module_name, mod = validate_module(self, context)
            if mod:
                self.directory = os.path.dirname(mod.__file__)
                self.module_name = module_name
            context.window_manager.fileselect_add(self)
            return {'RUNNING_MODAL'}
    
        def execute(self, context):
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            if not hasattr(self, "settings"):
                self.settings = settings.settings
            i18n_sett = context.window_manager.i18n_update_svn_settings
    
            module_name, mod = validate_module(self, context)
            if not (module_name and mod):
                return {'CANCELLED'}
    
            path = mod.__file__
            if path.endswith("__init__.py"):
                path = os.path.dirname(path)
    
            trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
    
            # Now search given dir, to find po's matching given languages...
            # Mapping po_uid: po_file.
            po_files = dict(utils_i18n.get_po_files_from_dir(self.directory))
    
            # Note: uids in i18n_sett.langs and addon's py code should be the same (both taken from the locale's languages
            #       file). So we just try to find the best match in po's for each enabled uid.
            for lng in i18n_sett.langs:
                if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
                    print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
                    continue
                if not lng.use:
                    print("Skipping {} language ({}).".format(lng.name, lng.uid))
                    continue
                uid = lng.uid
                po_uid = utils_i18n.find_best_isocode_matches(uid, po_files.keys())
                if not po_uid:
                    print("Skipping {} language, no PO file found for it ({}).".format(lng.name, uid))
                    continue
                po_uid = po_uid[0]
                msgs = utils_i18n.I18nMessages(uid=uid, kind='PO', key=uid, src=po_files[po_uid], settings=self.settings)
                if uid in trans.trans:
                    trans.trans[uid].merge(msgs, replace=True)
                else:
                    trans.trans[uid] = msgs
    
            trans.write(kind='PY')
    
            return {'FINISHED'}
    
    
    
    class UI_OT_i18n_addon_translation_export(Operator):
    
        """Export given add-on's translation data as PO files"""
    
        bl_idname = "ui.i18n_addon_translation_export"
    
        bl_label = "I18n Add-on Export"
    
        # Operator Arguments
        module_name: EnumProperty(
            name="Add-on",
            description="Add-on to process",
            items=enum_addons,
            options=set()
        )
    
        use_export_pot: BoolProperty(
            name="Export POT",
            description="Export (generate) a POT file too",
            default=True,
        )
    
        use_update_existing: BoolProperty(
            name="Update Existing",
            description="Update existing po files, if any, instead of overwriting them",
            default=True,
        )
    
        directory: StringProperty(
            subtype='FILE_PATH', maxlen=1024,
            options={'HIDDEN', 'SKIP_SAVE'}
        )
        # /End Operator Arguments
    
    
        def _dst(self, trans, path, uid, kind):
            if kind == 'PO':
                if uid == self.settings.PARSER_TEMPLATE_ID:
                    return os.path.join(self.directory, "blender.pot")
                path = os.path.join(self.directory, uid)
                if os.path.isdir(path):
                    return os.path.join(path, uid + ".po")
                return path + ".po"
            elif kind == 'PY':
                return trans._dst(trans, path, uid, kind)
            return path
    
        def invoke(self, context, event):
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            if not hasattr(self, "settings"):
                self.settings = settings.settings
            module_name, mod = validate_module(self, context)
            if mod:
                self.directory = os.path.dirname(mod.__file__)
                self.module_name = module_name
            context.window_manager.fileselect_add(self)
            return {'RUNNING_MODAL'}
    
        def execute(self, context):
    
            global _cached_enum_addons
            _cached_enum_addons[:] = []
    
            if not hasattr(self, "settings"):
                self.settings = settings.settings
            i18n_sett = context.window_manager.i18n_update_svn_settings
    
            module_name, mod = validate_module(self, context)
            if not (module_name and mod):
                return {'CANCELLED'}
    
            path = mod.__file__
            if path.endswith("__init__.py"):
                path = os.path.dirname(path)
    
            trans = utils_i18n.I18n(kind='PY', src=path, settings=self.settings)
            trans.dst = self._dst
    
            uids = [self.settings.PARSER_TEMPLATE_ID] if self.use_export_pot else []
            for lng in i18n_sett.langs:
                if lng.uid in self.settings.IMPORT_LANGUAGES_SKIP:
                    print("Skipping {} language ({}), edit settings if you want to enable it.".format(lng.name, lng.uid))
                    continue
                if not lng.use:
                    print("Skipping {} language ({}).".format(lng.name, lng.uid))
                    continue
                uid = utils_i18n.find_best_isocode_matches(lng.uid, trans.trans.keys())
                if uid:
                    uids.append(uid[0])
    
            # Try to update existing POs instead of overwriting them, if asked to do so!
            if self.use_update_existing:
                for uid in uids:
                    if uid == self.settings.PARSER_TEMPLATE_ID:
                        continue
    
                    path = trans.dst(trans, trans.src[uid], uid, 'PO')
    
                    if not os.path.isfile(path):
                        continue
                    msgs = utils_i18n.I18nMessages(kind='PO', src=path, settings=self.settings)
                    msgs.update(trans.msgs[self.settings.PARSER_TEMPLATE_ID])
                    trans.msgs[uid] = msgs
    
            trans.write(kind='PO', langs=set(uids))
    
            return {'FINISHED'}
    
    
    
    classes = (
        UI_OT_i18n_addon_translation_invoke,
        UI_OT_i18n_addon_translation_update,
        UI_OT_i18n_addon_translation_import,
        UI_OT_i18n_addon_translation_export,
    )