From b39c8c2650111a4dcd3417baf1742b6fac81a08b Mon Sep 17 00:00:00 2001 From: Bastien Montagne <montagne29@wanadoo.fr> Date: Sun, 24 Feb 2013 08:52:19 +0000 Subject: [PATCH] Big i18n tools update, II/II. Now everything should be done with ui_translate addon (which is also now fully functional again, in theory ;) ). Notes: * Everything is still a bit raw and sometimes hackish. * Not every feature implemented yet. * A bunch of cleanup is still needed. * Doc needs to be updated too! --- ui_translate/__init__.py | 311 +++---------------------------- ui_translate/edit_translation.py | 309 ++++++++++++++++++++++++++++++ ui_translate/settings.py | 196 +++++++++++++++++++ ui_translate/update_svn.py | 296 +++++++++++++++++++++++++++++ ui_translate/utils.py | 191 ------------------- 5 files changed, 826 insertions(+), 477 deletions(-) create mode 100644 ui_translate/edit_translation.py create mode 100644 ui_translate/settings.py create mode 100644 ui_translate/update_svn.py delete mode 100644 ui_translate/utils.py diff --git a/ui_translate/__init__.py b/ui_translate/__init__.py index da86a722a..673c4ece3 100644 --- a/ui_translate/__init__.py +++ b/ui_translate/__init__.py @@ -19,308 +19,47 @@ # <pep8 compliant> bl_info = { - "name": "Translate UI Messages", + "name": "Manage UI translations", "author": "Bastien Montagne", - "blender": (2, 63, 0), - "location": "Any UI control", - "description": "Allow to translate UI directly from Blender", - "warning": "Broken in this release!", - "wiki_url": "", - "tracker_url": "", + "blender": (2, 65, 10), + "location": "Main \"File\" menu, text editor, any UI control", + "description": "Allow to manage UI translations directly from Blender (update main po files, " + "update scripts' translations, etc.)", + "warning": "Still in development, not all features are fully implemented yet!", + "wiki_url": "http://wiki.blender.org/index.php/Dev:Doc/How_to/Translate_Blender/Addon", + "tracker_url": "http://projects.blender.org/tracker/?atid=498&group_id=9&func=browse", "support": 'OFFICIAL', "category": "System"} + if "bpy" in locals(): import imp - if "ui_utils" in locals(): - imp.reload(ui_utils) + imp.reload(settings) + imp.reload(edit_translation) + imp.reload(update_svn) else: import bpy - from bpy.props import (BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, - StringProperty, - ) - from . import utils as ui_utils + from . import settings + from . import edit_translation + from . import update_svn -from bl_i18n_utils import utils as i18n_utils -from bl_i18n_utils import update_mo -#from bl_i18n_utils import settings import os -import shutil - - -# module-level cache, as parsing po files takes a few seconds... -# Keys are po file paths, data are the results of i18n_utils.parse_messages(). -PO_CACHE = {} - - -def clear_caches(key): - del PO_CACHE[key] - del ui_utils.WORK_CACHE[key] - - -class UI_OT_edittranslation_update_mo(bpy.types.Operator): - """Try to "compile" given po file into relevant blender.mo file """ \ - """(WARNING: it will replace the official mo file in your user dir!)""" - bl_idname = "ui.edittranslation_update_mo" - bl_label = "Edit Translation Update Mo" - - # "Parameters" - lang = StringProperty(description="Current (translated) language", - options={'SKIP_SAVE'}) - po_file = StringProperty(description="Path to the matching po file", - subtype='FILE_PATH', options={'SKIP_SAVE'}) - clean_mo = BoolProperty(description="Clean up (remove) all local " - "translation files, to be able to use " - "all system's ones again", - default=False, options={'SKIP_SAVE'}) - - def execute(self, context): - if self.clean_mo: - root = bpy.utils.user_resource('DATAFILES', ui_utils.MO_PATH_ROOT) - if root: - shutil.rmtree(root) - - elif not self.lang or not self.po_file: - return {'CANCELLED'} - - else: - mo_dir = bpy.utils.user_resource( - 'DATAFILES', ui_utils.MO_PATH_TEMPLATE.format(self.lang), - create=True) - mo_file = os.path.join(mo_dir, ui_utils.MO_FILENAME) - update_mo.process_po(self.po_file, None, mo_file) - - bpy.ops.ui.reloadtranslation() - return {'FINISHED'} - - -class UI_OT_edittranslation(bpy.types.Operator): - """Translate the label and tool tip of the property defined by given 'parameters'""" - bl_idname = "ui.edittranslation" - bl_label = "Edit Translation" - - # "Parameters" - but_label = StringProperty(description="Label of the control", options={'SKIP_SAVE'}) - rna_label = StringProperty(description="RNA-defined label of the control, if any", options={'SKIP_SAVE'}) - enum_label = StringProperty(description="Label of the enum item of the control, if any", options={'SKIP_SAVE'}) - but_tip = StringProperty(description="Tip of the control", options={'SKIP_SAVE'}) - rna_tip = StringProperty(description="RNA-defined tip of the control, if any", options={'SKIP_SAVE'}) - enum_tip = StringProperty(description="Tip of the enum item of the control, if any", options={'SKIP_SAVE'}) - rna_struct = StringProperty(description="Identifier of the RNA struct, if any", options={'SKIP_SAVE'}) - rna_prop = StringProperty(description="Identifier of the RNA property, if any", options={'SKIP_SAVE'}) - rna_enum = StringProperty(description="Identifier of the RNA enum item, if any", options={'SKIP_SAVE'}) - rna_ctxt = StringProperty(description="RNA context for label", options={'SKIP_SAVE'}) - - lang = StringProperty(description="Current (translated) language", options={'SKIP_SAVE'}) - po_file = StringProperty(description="Path to the matching po file", subtype='FILE_PATH', options={'SKIP_SAVE'}) - - # Found in po file. - org_but_label = StringProperty(description="Original label of the control", options={'SKIP_SAVE'}) - org_rna_label = StringProperty(description="Original RNA-defined label of the control, if any", options={'SKIP_SAVE'}) - org_enum_label = StringProperty(description="Original label of the enum item of the control, if any", options={'SKIP_SAVE'}) - org_but_tip = StringProperty(description="Original tip of the control", options={'SKIP_SAVE'}) - org_rna_tip = StringProperty(description="Original RNA-defined tip of the control, if any", options={'SKIP_SAVE'}) - org_enum_tip = StringProperty(description="Original tip of the enum item of the control, if any", options={'SKIP_SAVE'}) - - flag_items = (('FUZZY', "Fuzzy", "Message is marked as fuzzy in po file"), - ('ERROR', "Error", "Some error occurred with this message"), - ) - but_label_flags = EnumProperty(items=flag_items, description="Flags about the label of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - rna_label_flags = EnumProperty(items=flag_items, description="Flags about the RNA-defined label of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - enum_label_flags = EnumProperty(items=flag_items, description="Flags about the RNA enum item label of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - but_tip_flags = EnumProperty(items=flag_items, description="Flags about the tip of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - rna_tip_flags = EnumProperty(items=flag_items, description="Flags about the RNA-defined tip of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - enum_tip_flags = EnumProperty(items=flag_items, description="Flags about the RNA enum item tip of the button", options={'SKIP_SAVE', 'ENUM_FLAG'}) - - stats_str = StringProperty(description="Stats from opened po", options={'SKIP_SAVE'}) - update_po = BoolProperty(description="Update po file, try to rebuild mo file, and refresh Blender UI", default=False, options={'SKIP_SAVE'}) - update_mo = BoolProperty(description="Try to rebuild mo file, and refresh Blender UI (WARNING: you should use a local Blender installation, as you probably have no right to write in the system Blender installation...)", default=False, options={'SKIP_SAVE'}) - clean_mo = BoolProperty(description="Clean up (remove) all local " - "translation files, to be able to use " - "all system's ones again", - default=False, options={'SKIP_SAVE'}) - - def execute(self, context): - if not hasattr(self, "msgmap"): - # We must be invoked() first! - return {'CANCELLED'} - msgs, state, stats = PO_CACHE[self.po_file] - - done_keys = set() - for mmap in self.msgmap.values(): - if 'ERROR' in getattr(self, mmap["msg_flags"]): - continue - k = mmap["key"] -# print(k) - if k not in done_keys and len(k) == 1: - k = tuple(k)[0] - msgs[k]["msgstr_lines"] = [getattr(self, mmap["msgstr"])] - if k in state["fuzzy_msg"] and 'FUZZY' not in getattr(self, mmap["msg_flags"]): - state["fuzzy_msg"].remove(k) - elif k not in state["fuzzy_msg"] and 'FUZZY' in getattr(self, mmap["msg_flags"]): - state["fuzzy_msg"].add(k) - done_keys.add(k) - - if self.update_po: - # Try to overwrite po file, may fail if we have no good rights... - try: - i18n_utils.write_messages(self.po_file, msgs, state["comm_msg"], state["fuzzy_msg"]) - except Exception as e: - self.report('ERROR', "Could not write to po file ({})".format(str(e))) - # Always invalidate all caches afterward! - clear_caches(self.po_file) - if self.update_mo: - lang = os.path.splitext(os.path.basename(self.po_file))[0] - bpy.ops.ui.edittranslation_update_mo(po_file=self.po_file, lang=lang) - elif self.clean_mo: - bpy.ops.ui.edittranslation_update_mo(clean_mo=True) - return {'FINISHED'} - - def invoke(self, context, event): - if self.po_file in PO_CACHE: - msgs, state, stats = PO_CACHE[self.po_file] - else: - msgs, state, stats = PO_CACHE.setdefault(self.po_file, i18n_utils.parse_messages(self.po_file)) - - self.msgmap = {"but_label": {"msgstr": "but_label", "msgid": "org_but_label", "msg_flags": "but_label_flags", "key": set()}, - "rna_label": {"msgstr": "rna_label", "msgid": "org_rna_label", "msg_flags": "rna_label_flags", "key": set()}, - "enum_label": {"msgstr": "enum_label", "msgid": "org_enum_label", "msg_flags": "enum_label_flags", "key": set()}, - "but_tip": {"msgstr": "but_tip", "msgid": "org_but_tip", "msg_flags": "but_tip_flags", "key": set()}, - "rna_tip": {"msgstr": "rna_tip", "msgid": "org_rna_tip", "msg_flags": "rna_tip_flags", "key": set()}, - "enum_tip": {"msgstr": "enum_tip", "msgid": "org_enum_tip", "msg_flags": "enum_tip_flags", "key": set()}, - } - - ui_utils.find_best_msgs_matches(self, self.po_file, self.msgmap, msgs, state, self.rna_ctxt, - self.rna_struct, self.rna_prop, self.rna_enum) - self.stats_str = "{}: {} messages, {} translated.".format(os.path.basename(self.po_file), stats["tot_msg"], stats["trans_msg"]) - - for mmap in self.msgmap.values(): - k = tuple(mmap["key"]) - if k: - if len(k) == 1: - k = k[0] - ctxt, msgid = k - setattr(self, mmap["msgstr"], "".join(msgs[k]["msgstr_lines"])) - setattr(self, mmap["msgid"], msgid) - if k in state["fuzzy_msg"]: - setattr(self, mmap["msg_flags"], {'FUZZY'}) - else: - setattr(self, mmap["msgid"], "ERROR: Button label “{}” matches none or several messages in po file ({})!".format(self.but_label, k)) - setattr(self, mmap["msg_flags"], {'ERROR'}) - else: - setattr(self, mmap["msgstr"], "") - setattr(self, mmap["msgid"], "") - - wm = context.window_manager - return wm.invoke_props_dialog(self, width=600) - - def draw(self, context): - layout = self.layout - layout.label(text=self.stats_str) - src, _a, _b = ui_utils.bpy_path(self.rna_struct, self.rna_prop, self.rna_enum) - if src: - layout.label(text=" RNA Path: bpy.types." + src) - if self.rna_ctxt: - layout.label(text=" RNA Context: " + self.rna_ctxt) - - if self.org_but_label or self.org_rna_label or self.org_enum_label: - # XXX Can't use box, labels are not enought readable in them :/ -# box = layout.box() - box = layout - box.label(text="Labels:") - split = box.split(percentage=0.15) - col1 = split.column() - col2 = split.column() - if self.org_but_label: - col1.label(text="Button Label:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.but_label_flags: - row.alert = True - else: - col1.prop_enum(self, "but_label_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "but_label", text="") - row.prop(self, "org_but_label", text="") - if self.org_rna_label: - col1.label(text="RNA Label:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.rna_label_flags: - row.alert = True - else: - col1.prop_enum(self, "rna_label_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "rna_label", text="") - row.prop(self, "org_rna_label", text="") - if self.org_enum_label: - col1.label(text="Enum Item Label:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.enum_label_flags: - row.alert = True - else: - col1.prop_enum(self, "enum_label_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "enum_label", text="") - row.prop(self, "org_enum_label", text="") - - if self.org_but_tip or self.org_rna_tip or self.org_enum_tip: - # XXX Can't use box, labels are not enought readable in them :/ -# box = layout.box() - box = layout - box.label(text="Tool Tips:") - split = box.split(percentage=0.15) - col1 = split.column() - col2 = split.column() - if self.org_but_tip: - col1.label(text="Button Tip:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.but_tip_flags: - row.alert = True - else: - col1.prop_enum(self, "but_tip_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "but_tip", text="") - row.prop(self, "org_but_tip", text="") - if self.org_rna_tip: - col1.label(text="RNA Tip:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.rna_tip_flags: - row.alert = True - else: - col1.prop_enum(self, "rna_tip_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "rna_tip", text="") - row.prop(self, "org_rna_tip", text="") - if self.org_enum_tip: - col1.label(text="Enum Item Tip:") - row = col2.row() - row.enabled = False - if 'ERROR' in self.enum_tip_flags: - row.alert = True - else: - col1.prop_enum(self, "enum_tip_flags", 'FUZZY', text="Fuzzy") - col2.prop(self, "enum_tip", text="") - row.prop(self, "org_enum_tip", text="") - - row = layout.row() - row.prop(self, "update_po", text="Save to PO File", toggle=True) - row.prop(self, "update_mo", text="Rebuild MO File", toggle=True) - row.prop(self, "clean_mo", text="Erase Local MO files", toggle=True) def register(): bpy.utils.register_module(__name__) + bpy.types.WindowManager.i18n_update_svn_settings = \ + bpy.props.PointerProperty(type=update_svn.I18nUpdateTranslationSettings) + + # Init addon's preferences (unfortunately, as we are using an external storage for the properties, + # the load/save user preferences process has no effect on them :( ). + if __name__ in bpy.context.user_preferences.addons: + pref = bpy.context.user_preferences.addons[__name__].preferences + if os.path.isfile(pref.persistent_data_path): + pref._settings.load(pref.persistent_data_path, reset=True) def unregister(): + del bpy.types.WindowManager.i18n_update_svn_settings bpy.utils.unregister_module(__name__) - - -if __name__ == "__main__": - register() diff --git a/ui_translate/edit_translation.py b/ui_translate/edit_translation.py new file mode 100644 index 000000000..02cde75bd --- /dev/null +++ b/ui_translate/edit_translation.py @@ -0,0 +1,309 @@ +# ##### 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 imp + imp.reload(settings) + imp.reload(i18n_utils) +else: + import bpy + from bpy.props import (BoolProperty, + CollectionProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + StringProperty, + ) + from . import settings + from bl_i18n_utils import utils as i18n_utils + + +import os +import shutil + + +# A global cache for I18nMessages objects, as parsing po files takes a few seconds. +PO_CACHE = {} + + +def _get_messages(lang, fname): + if fname not in PO_CACHE: + PO_CACHE[fname] = i18n_utils.I18nMessages(uid=lang, kind='PO', key=fname, src=fname, settings=settings.settings) + return PO_CACHE[fname] + + +class UI_OT_i18n_edittranslation_update_mo(bpy.types.Operator): + """Try to "compile" given po file into relevant blender.mo file """ \ + """(WARNING: it will replace the official mo file in your user dir!)""" + bl_idname = "ui.i18n_edittranslation_update_mo" + bl_label = "Edit Translation Update Mo" + + # "Parameters" + lang = StringProperty(description="Current (translated) language", + options={'SKIP_SAVE'}) + po_file = StringProperty(description="Path to the matching po file", + subtype='FILE_PATH', options={'SKIP_SAVE'}) + clean_mo = BoolProperty(description="Clean up (remove) all local " + "translation files, to be able to use " + "all system's ones again", + default=False, options={'SKIP_SAVE'}) + + def execute(self, context): + if self.clean_mo: + root = bpy.utils.user_resource('DATAFILES', settings.settings.MO_PATH_ROOT_RELATIVE) + if root: + shutil.rmtree(root) + elif not (self.lang and self.po_file): + return {'CANCELLED'} + else: + mo_dir = bpy.utils.user_resource('DATAFILES', settings.settings.MO_PATH_TEMPLATE_RELATIVE.format(self.lang), + create=True) + mo_file = os.path.join(mo_dir, settings.settings.MO_FILE_NAME) + _get_messages(self.lang, self.po_file).write(kind='MO', dest=mo_file) + + bpy.ops.ui.reloadtranslation() + return {'FINISHED'} + + +class UI_OT_i18n_edittranslation(bpy.types.Operator): + """Translate the label and tool tip of the property defined by given 'parameters'""" + bl_idname = "ui.edittranslation" + bl_label = "Edit Translation" + + # "Parameters" + but_label = StringProperty(description="Label of the control", options={'SKIP_SAVE'}) + rna_label = StringProperty(description="RNA-defined label of the control, if any", options={'SKIP_SAVE'}) + enum_label = StringProperty(description="Label of the enum item of the control, if any", options={'SKIP_SAVE'}) + but_tip = StringProperty(description="Tip of the control", options={'SKIP_SAVE'}) + rna_tip = StringProperty(description="RNA-defined tip of the control, if any", options={'SKIP_SAVE'}) + enum_tip = StringProperty(description="Tip of the enum item of the control, if any", options={'SKIP_SAVE'}) + rna_struct = StringProperty(description="Identifier of the RNA struct, if any", options={'SKIP_SAVE'}) + rna_prop = StringProperty(description="Identifier of the RNA property, if any", options={'SKIP_SAVE'}) + rna_enum = StringProperty(description="Identifier of the RNA enum item, if any", options={'SKIP_SAVE'}) + rna_ctxt = StringProperty(description="RNA context for label", options={'SKIP_SAVE'}) + + lang = StringProperty(description="Current (translated) language", options={'SKIP_SAVE'}) + po_file = StringProperty(description="Path to the matching po file", subtype='FILE_PATH', options={'SKIP_SAVE'}) + + # Found in po file. + org_but_label = StringProperty(description="Original label of the control", options={'SKIP_SAVE'}) + org_rna_label = StringProperty(description="Original RNA-defined label of the control, if any", + options={'SKIP_SAVE'}) + org_enum_label = StringProperty(description="Original label of the enum item of the control, if any", + options={'SKIP_SAVE'}) + org_but_tip = StringProperty(description="Original tip of the control", options={'SKIP_SAVE'}) + org_rna_tip = StringProperty(description="Original RNA-defined tip of the control, if any", options={'SKIP_SAVE'}) + org_enum_tip = StringProperty(description="Original tip of the enum item of the control, if any", + options={'SKIP_SAVE'}) + + flag_items = (('FUZZY', "Fuzzy", "Message is marked as fuzzy in po file"), + ('ERROR', "Error", "Some error occurred with this message"), + ) + but_label_flags = EnumProperty(items=flag_items, description="Flags about the label of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + rna_label_flags = EnumProperty(items=flag_items, description="Flags about the RNA-defined label of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + enum_label_flags = EnumProperty(items=flag_items, description="Flags about the RNA enum item label of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + but_tip_flags = EnumProperty(items=flag_items, description="Flags about the tip of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + rna_tip_flags = EnumProperty(items=flag_items, description="Flags about the RNA-defined tip of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + enum_tip_flags = EnumProperty(items=flag_items, description="Flags about the RNA enum item tip of the button", + options={'SKIP_SAVE', 'ENUM_FLAG'}) + + stats_str = StringProperty(description="Stats from opened po", options={'SKIP_SAVE'}) + update_po = BoolProperty(description="Update po file, try to rebuild mo file, and refresh Blender UI", + default=False, options={'SKIP_SAVE'}) + update_mo = BoolProperty(description="Try to rebuild mo file, and refresh Blender UI", + default=False, options={'SKIP_SAVE'}) + clean_mo = BoolProperty(description="Clean up (remove) all local translation files, to be able to use " + "all system's ones again", + default=False, options={'SKIP_SAVE'}) + + def execute(self, context): + if not hasattr(self, "msgmap"): + self.report('ERROR', "Looks like you did not invoke this operator first!") + return {'CANCELLED'} + + msgs = _get_messages(self.lang, self.po_file) + done_keys = set() + for mmap in self.msgmap.values(): + if 'ERROR' in getattr(self, mmap["msg_flags"]): + continue + k = mmap["key"] +# print(k) + if k not in done_keys and len(k) == 1: + k = tuple(k)[0] + msgs.msgs[k].msgstr = getattr(self, mmap["msgstr"]) + msgs.msgs[k].is_fuzzy = 'FUZZY' in getattr(self, mmap["msg_flags"]) + done_keys.add(k) + + if self.update_po: + # Try to overwrite po file, may fail if we have no good rights... + try: + msgs.write(kind='PO', dest=self.po_file) + except Exception as e: + self.report('ERROR', "Could not write to po file ({})".format(str(e))) + # Always invalidate reverse messages cache afterward! + msgs.invalidate_reverse_cache() + if self.update_mo: + lang = os.path.splitext(os.path.basename(self.po_file))[0] + bpy.ops.ui.i18n_edittranslation_update_mo(po_file=self.po_file, lang=lang) + elif self.clean_mo: + bpy.ops.ui.i18n_edittranslation_update_mo(clean_mo=True) + return {'FINISHED'} + + def invoke(self, context, event): + self.msgmap = {"but_label": {"msgstr": "but_label", "msgid": "org_but_label", + "msg_flags": "but_label_flags", "key": set()}, + "rna_label": {"msgstr": "rna_label", "msgid": "org_rna_label", + "msg_flags": "rna_label_flags", "key": set()}, + "enum_label": {"msgstr": "enum_label", "msgid": "org_enum_label", + "msg_flags": "enum_label_flags", "key": set()}, + "but_tip": {"msgstr": "but_tip", "msgid": "org_but_tip", + "msg_flags": "but_tip_flags", "key": set()}, + "rna_tip": {"msgstr": "rna_tip", "msgid": "org_rna_tip", + "msg_flags": "rna_tip_flags", "key": set()}, + "enum_tip": {"msgstr": "enum_tip", "msgid": "org_enum_tip", + "msg_flags": "enum_tip_flags", "key": set()}, + } + + msgs = _get_messages(self.lang, self.po_file) + msgs.find_best_messages_matches(self, self.msgmap, self.rna_ctxt, self.rna_struct, self.rna_prop, self.rna_enum) + msgs.update_info() + self.stats_str = "{}: {} messages, {} translated.".format(os.path.basename(self.po_file), msgs.nbr_msgs, + msgs.nbr_trans_msgs) + + for mmap in self.msgmap.values(): + k = tuple(mmap["key"]) + if k: + if len(k) == 1: + k = k[0] + ctxt, msgid = k + setattr(self, mmap["msgstr"], msgs.msgs[k].msgstr) + setattr(self, mmap["msgid"], msgid) + if msgs.msgs[k].is_fuzzy: + setattr(self, mmap["msg_flags"], {'FUZZY'}) + else: + setattr(self, mmap["msgid"], + "ERROR: Button label “{}” matches several messages in po file ({})!" + "".format(self.but_label, k)) + setattr(self, mmap["msg_flags"], {'ERROR'}) + else: + setattr(self, mmap["msgstr"], "") + setattr(self, mmap["msgid"], "") + + wm = context.window_manager + return wm.invoke_props_dialog(self, width=600) + + def draw(self, context): + layout = self.layout + layout.label(text=self.stats_str) + src, _a, _b = bpy.utils.make_rna_paths(self.rna_struct, self.rna_prop, self.rna_enum) + if src: + layout.label(text=" RNA Path: bpy.types." + src) + if self.rna_ctxt: + layout.label(text=" RNA Context: " + self.rna_ctxt) + + if self.org_but_label or self.org_rna_label or self.org_enum_label: + # XXX Can't use box, labels are not enough readable in them :/ + box = layout.box() + #box = layout + box.label(text="Labels:") + split = box.split(percentage=0.15) + col1 = split.column() + col2 = split.column() + if self.org_but_label: + col1.label(text="Button Label:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.but_label_flags: + row.alert = True + else: + col1.prop_enum(self, "but_label_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "but_label", text="") + row.prop(self, "org_but_label", text="") + if self.org_rna_label: + col1.label(text="RNA Label:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.rna_label_flags: + row.alert = True + else: + col1.prop_enum(self, "rna_label_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "rna_label", text="") + row.prop(self, "org_rna_label", text="") + if self.org_enum_label: + col1.label(text="Enum Item Label:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.enum_label_flags: + row.alert = True + else: + col1.prop_enum(self, "enum_label_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "enum_label", text="") + row.prop(self, "org_enum_label", text="") + + if self.org_but_tip or self.org_rna_tip or self.org_enum_tip: + # XXX Can't use box, labels are not enough readable in them :/ + box = layout.box() + #box = layout + box.label(text="Tool Tips:") + split = box.split(percentage=0.15) + col1 = split.column() + col2 = split.column() + if self.org_but_tip: + col1.label(text="Button Tip:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.but_tip_flags: + row.alert = True + else: + col1.prop_enum(self, "but_tip_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "but_tip", text="") + row.prop(self, "org_but_tip", text="") + if self.org_rna_tip: + col1.label(text="RNA Tip:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.rna_tip_flags: + row.alert = True + else: + col1.prop_enum(self, "rna_tip_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "rna_tip", text="") + row.prop(self, "org_rna_tip", text="") + if self.org_enum_tip: + col1.label(text="Enum Item Tip:") + row = col2.row() + row.enabled = False + if 'ERROR' in self.enum_tip_flags: + row.alert = True + else: + col1.prop_enum(self, "enum_tip_flags", 'FUZZY', text="Fuzzy") + col2.prop(self, "enum_tip", text="") + row.prop(self, "org_enum_tip", text="") + + row = layout.row() + row.prop(self, "update_po", text="Save to PO File", toggle=True) + row.prop(self, "update_mo", text="Rebuild MO File", toggle=True) + row.prop(self, "clean_mo", text="Erase Local MO files", toggle=True) diff --git a/ui_translate/settings.py b/ui_translate/settings.py new file mode 100644 index 000000000..bd971f443 --- /dev/null +++ b/ui_translate/settings.py @@ -0,0 +1,196 @@ +# ##### 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 imp + imp.reload(i18n_settings) +else: + import bpy + from bpy.props import (BoolProperty, + CollectionProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + StringProperty, + ) + from bl_i18n_utils import settings as i18n_settings + + +import os + + +settings = i18n_settings.I18nSettings() + + +class UI_OT_i18n_settings_load(bpy.types.Operator): + """Load translations' settings from a persistent JSon file""" + bl_idname = "ui.i18n_settings_load" + bl_label = "I18n Load Settings" + bl_option = {'REGISTER'} + + # "Parameters" + filepath = StringProperty(description="Path to the saved settings file", + subtype='FILE_PATH') + filter_glob = StringProperty(default="*.json", options={'HIDDEN'}) + + def invoke(self, context, event): + if not self.properties.is_property_set("filepath"): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + else: + return self.execute(context) + + def execute(self, context): + if not (self.filepath and settings): + return {'CANCELLED'} + settings.load(self.filepath, reset=True) + return {'FINISHED'} + + +class UI_OT_i18n_settings_save(bpy.types.Operator): + """Save translations' settings in a persistent JSon file""" + bl_idname = "ui.i18n_settings_save" + bl_label = "I18n Save Settings" + bl_option = {'REGISTER'} + + # "Parameters" + filepath = StringProperty(description="Path to the saved settings file", + subtype='FILE_PATH') + filter_glob = StringProperty(default="*.json", options={'HIDDEN'}) + + def invoke(self, context, event): + if not self.properties.is_property_set("filepath"): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + else: + return self.execute(context) + + def execute(self, context): + if not (self.filepath and settings): + return {'CANCELLED'} + settings.save(self.filepath) + return {'FINISHED'} + + +def _setattr(self, name, val): + print(self, name, val) + setattr(self, name, val) + +class UI_AP_i18n_settings(bpy.types.AddonPreferences): + bl_idname = __name__.split(".")[0] # We want "top" module name! + bl_option = {'REGISTER'} + + _settings = settings + + WARN_MSGID_NOT_CAPITALIZED = BoolProperty( + name="Warn Msgid Not Capitalized", + description="Warn about messages not starting by a capitalized letter (with a few allowed exceptions!)", + default=True, + get=lambda self: self._settings.WARN_MSGID_NOT_CAPITALIZED, + set=lambda self, val: _setattr(self._settings, "WARN_MSGID_NOT_CAPITALIZED", val), + ) + + GETTEXT_MSGFMT_EXECUTABLE = StringProperty( + name="Gettext 'msgfmt' executable", + description="The gettext msgfmt 'compiler'. You’ll likely have to edit it if you’re under Windows", + subtype='FILE_PATH', + default="msgfmt", + get=lambda self: self._settings.GETTEXT_MSGFMT_EXECUTABLE, + set=lambda self, val: setattr(self._settings, "GETTEXT_MSGFMT_EXECUTABLE", val), + ) + + FRIBIDI_LIB = StringProperty( + name="Fribidi Library", + description="The FriBidi C compiled library (.so under Linux, .dll under windows...), you’ll likely have " + "to edit it if you’re under Windows, e.g. using the one included in svn's libraries repository", + subtype='FILE_PATH', + default="libfribidi.so.0", + get=lambda self: self._settings.FRIBIDI_LIB, + set=lambda self, val: setattr(self._settings, "FRIBIDI_LIB", val), + ) + + SOURCE_DIR = StringProperty( + name="Source Root", + description="The Blender source root path", + subtype='FILE_PATH', + default="blender", + get=lambda self: self._settings.SOURCE_DIR, + set=lambda self, val: setattr(self._settings, "SOURCE_DIR", val), + ) + + I18N_DIR = StringProperty( + name="Translation Root", + description="The bf-translation repository", + subtype='FILE_PATH', + default="i18n", + get=lambda self: self._settings.I18N_DIR, + set=lambda self, val: setattr(self._settings, "I18N_DIR", val), + ) + + SPELL_CACHE = StringProperty( + name="Spell Cache", + description="A cache storing validated msgids, to avoid re-spellchecking them", + subtype='FILE_PATH', + default=os.path.join("/tmp", ".spell_cache"), + get=lambda self: self._settings.SPELL_CACHE, + set=lambda self, val: setattr(self._settings, "SPELL_CACHE", val), + ) + + PY_SYS_PATHS = StringProperty( + name="Import Paths", + description="Additional paths to add to sys.path (';' separated)", + default="", + get=lambda self: self._settings.PY_SYS_PATHS, + set=lambda self, val: setattr(self._settings, "PY_SYS_PATHS", val), + ) + + persistent_data_path = StringProperty( + name="Persistent Data Path", + description="The name of a json file storing those settings (unfortunately, Blender's system " + "does not work here)", + subtype='FILE_PATH', + default=os.path.join("ui_translate_settings.json"), + ) + _is_init = False + + def draw(self, context): + layout = self.layout + layout.label(text="WARNING: preferences are lost when addon is disabled, be sure to use \"Save Persistent\" " + "if you want to keep your settings!") + layout.prop(self, "WARN_MSGID_NOT_CAPITALIZED") + layout.prop(self, "GETTEXT_MSGFMT_EXECUTABLE") + layout.prop(self, "FRIBIDI_LIB") + layout.prop(self, "SOURCE_DIR") + layout.prop(self, "I18N_DIR") + layout.prop(self, "SPELL_CACHE") + layout.prop(self, "PY_SYS_PATHS") + + layout.separator() + split = layout.split(0.75) + col = split.column() + col.prop(self, "persistent_data_path") + row = col.row() + row.operator("UI_OT_i18n_settings_save", text="Save").filepath = self.persistent_data_path + row.operator("UI_OT_i18n_settings_load", text="Load").filepath = self.persistent_data_path + col = split.column() + col.operator("UI_OT_i18n_settings_save", text="Save Persistent To...") + col.operator("UI_OT_i18n_settings_load", text="Load Persistent From...") diff --git a/ui_translate/update_svn.py b/ui_translate/update_svn.py new file mode 100644 index 000000000..946e7e4f7 --- /dev/null +++ b/ui_translate/update_svn.py @@ -0,0 +1,296 @@ +# ##### 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 imp + imp.reload(settings) + imp.reload(i18n_utils) + imp.reload(languages_menu_utils) +else: + import bpy + from bpy.props import (BoolProperty, + CollectionProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + StringProperty, + ) + from . import settings + from bl_i18n_utils import utils as i18n_utils + from bl_i18n_utils import languages_menu_utils + +from bpy.app.translations import pgettext_iface as iface_ + +import os +import shutil +import subprocess +import tempfile + +##### Helpers ##### +def find_best_isocode_matches(uid, iso_codes): + tmp = ((e, i18n_utils.locale_match(e, uid)) for e in iso_codes) + return tuple(e[0] for e in sorted((e for e in tmp if e[1] is not ... and e[1] >= 0), key=lambda e: e[1])) + + +##### Data ##### +class I18nUpdateTranslationLanguage(bpy.types.PropertyGroup): + """Settings/info about a language""" + uid = StringProperty(name="Language ID", default="", description="Iso code, like fr_FR") + num_id = IntProperty(name="Numeric ID", default=0, min=0, description="Numeric ID (readonly!)") + name = StringProperty(name="Language Name", default="", + description="English language name/label (like \"French (Français)\")") + use = BoolProperty(name="Use", default=True, description="Use this language in current operator") + po_path = StringProperty(name="PO File Path", default="", subtype='FILE_PATH', + description="Path to the relevant po file in branches") + po_path_trunk = StringProperty(name="PO Trunk File Path", default="", subtype='FILE_PATH', + description="Path to the relevant po file in trunk") + mo_path_trunk = StringProperty(name="MO File Path", default="", subtype='FILE_PATH', + description="Path to the relevant mo file") + + +class I18nUpdateTranslationSettings(bpy.types.PropertyGroup): + """Settings/info about a language""" + langs = CollectionProperty(name="Languages", type=I18nUpdateTranslationLanguage, + description="Languages to update in branches") + active_lang = IntProperty(name="Active Language", default=0, + description="Index of active language in langs collection") + pot_path = StringProperty(name="POT File Path", default="", subtype='FILE_PATH', + description="Path to the pot template file") + is_init = BoolProperty(default=False, options={'HIDDEN'}, + description="Whether these settings have already been auto-set or not") + + +##### UI ##### +class UI_UL_i18n_languages(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + #assert(isinstance(item, bpy.types.I18nUpdateTranslationLanguage)) + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.label(item.name, icon_value=icon) + layout.prop(item, "use", text="") + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(item.uid) + layout.prop(item, "use", text="") + + +class UI_PT_i18n_update_translations_settings(bpy.types.Panel): + bl_label = "I18n Update Translation Main" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + def draw(self, context): + layout = self.layout + i18n_sett = context.window_manager.i18n_update_svn_settings + + if not i18n_sett.is_init and bpy.ops.ui.i18n_updatetranslation_svn_init_settings.poll(): + bpy.ops.ui.i18n_updatetranslation_svn_init_settings() + + if not i18n_sett.is_init: + layout.label(text="Could not init languages data!") + layout.label(text="Please edit the preferences of the UI Translate addon") + else: + split = layout.split(0.75) + split.template_list("UI_UL_i18n_languages", "", i18n_sett, "langs", i18n_sett, "active_lang", rows=5) + col = split.column() + col.operator("ui.i18n_updatetranslation_svn_settings_select_all", text="Select All").use_select = True + col.operator("ui.i18n_updatetranslation_svn_settings_select_all", text="Deselect All").use_select = False + + if i18n_sett.active_lang >= 0 and i18n_sett.active_lang < len(i18n_sett.langs): + lng = i18n_sett.langs[i18n_sett.active_lang] + col = layout.column() + col.active = lng.use + row = col.row() + row.label(text="[{}]: \"{}\" ({})".format(lng.uid, iface_(lng.name), lng.num_id), translate=False) + row.prop(lng, "use", text="") + col.prop(lng, "po_path") + col.prop(lng, "po_path_trunk") + col.prop(lng, "mo_path_trunk") + layout.separator() + layout.prop(i18n_sett, "pot_path") + row = layout.row() + row.operator("ui.i18n_updatetranslation_svn_init_settings", text="Reset Settings") + row.operator("ui.i18n_updatetranslation_svn_branches", text="Update Branches") + row.operator("ui.i18n_updatetranslation_svn_trunk", text="Update Trunk") + + +##### Operators ##### +class UI_OT_i18n_updatetranslation_svn_init_settings(bpy.types.Operator): + """Init settings for i18n svn's update operators""" + bl_idname = "ui.i18n_updatetranslation_svn_init_settings" + bl_label = "Init I18n Update Settings" + bl_option = {'REGISTER'} + + @classmethod + def poll(cls, context): + return context.window_manager != None + + def execute(self, context): + if not hasattr(self, "settings"): + self.settings = settings.settings + i18n_sett = context.window_manager.i18n_update_svn_settings + + # First, create the list of languages from settings. + i18n_sett.langs.clear() + root_br = self.settings.BRANCHES_DIR + root_tr_po = self.settings.TRUNK_PO_DIR + root_tr_mo = os.path.join(self.settings.TRUNK_DIR, self.settings.MO_PATH_TEMPLATE, self.settings.MO_FILE_NAME) + if not (os.path.isdir(root_br) and os.path.isdir(root_tr_po)): + return {'CANCELLED'} + isocodes = ((e, os.path.join(root_br, e, e + ".po")) for e in os.listdir(root_br)) + isocodes = dict(e for e in isocodes if os.path.isfile(e[1])) + for num_id, name, uid in self.settings.LANGUAGES[2:]: # Skip "default" and "en" languages! + best_po = find_best_isocode_matches(uid, isocodes) + #print(uid, "->", best_po) + lng = i18n_sett.langs.add() + lng.uid = uid + lng.num_id = num_id + lng.name = name + if best_po: + lng.use = True + isocode = best_po[0] + lng.po_path = isocodes[isocode] + lng.po_path_trunk = os.path.join(root_tr_po, isocode + ".po") + lng.mo_path_trunk = root_tr_mo.format(isocode) + else: + lng.use = False + language, _1, _2, language_country, language_variant = i18n_utils.locale_explode(uid) + for isocode in (language, language_variant, language_country, uid): + p = os.path.join(root_br, isocode, isocode + ".po") + if not os.path.exists(p): + lng.use = True + lng.po_path = p + lng.po_path_trunk = os.path.join(root_tr_po, isocode + ".po") + lng.mo_path_trunk = root_tr_mo.format(isocode) + break + + i18n_sett.pot_path = self.settings.FILE_NAME_POT + i18n_sett.is_init = True + return {'FINISHED'} + + +class UI_OT_i18n_updatetranslation_svn_settings_select_all(bpy.types.Operator): + """(De)select all languages for i18n svn's update operators""" + bl_idname = "ui.i18n_updatetranslation_svn_settings_select_all" + bl_label = "Init I18n Update Select Languages" + + use_select = BoolProperty(default=True, description="Select all if True, else deselect all") + + @classmethod + def poll(cls, context): + return context.window_manager != None + + def execute(self, context): + for lng in context.window_manager.i18n_update_svn_settings.langs: + lng.use = self.use_select + return {'FINISHED'} + + +class UI_OT_i18n_updatetranslation_svn_branches(bpy.types.Operator): + """Update i18n svn's branches (po files)""" + bl_idname = "ui.i18n_updatetranslation_svn_branches" + bl_label = "Update I18n Branches" + + def execute(self, context): + if not hasattr(self, "settings"): + self.settings = settings.settings + i18n_sett = context.window_manager.i18n_update_svn_settings + self.settings.FILE_NAME_POT = i18n_sett.pot_path + # Generate base pot from RNA messages (we use another blender instance here, to be able to perfectly + # control our environment (factory startup, specific addons enabled/disabled...)). + # However, we need to export current user settings about this addon! + cmmd = ( + bpy.app.binary_path, + "--background", + "--factory-startup", + "--python", + os.path.join(os.path.dirname(i18n_utils.__file__), "bl_extract_messages.py"), + "--", + "bl_extract_messages.py", # arg parser expects first arg to be prog name! + "--settings", + self.settings.to_json(), + ) + if subprocess.call(cmmd): + self.report({'ERROR'}, "Message extraction process failed!") + return {'CANCELLED'} + # Now we should have a valid POT file, we have to merge it in all languages po's... + pot = i18n_utils.I18nMessages(kind='PO', src=self.settings.FILE_NAME_POT, settings=self.settings) + for lng in i18n_sett.langs: + if not lng.use: + continue + if os.path.isfile(lng.po_path): + po = i18n_utils.I18nMessages(uid=lng.uid, kind='PO', src=lng.po_path, settings=self.settings) + po.update(pot) + else: + po = pot + po.write(kind="PO", dest=lng.po_path) + print("{} PO written!".format(lng.uid)) + return {'FINISHED'} + + +class UI_OT_i18n_updatetranslation_svn_trunk(bpy.types.Operator): + """Update i18n svn's branches (po files)""" + bl_idname = "ui.i18n_updatetranslation_svn_trunk" + bl_label = "Update I18n Trunk" + + def execute(self, context): + if not hasattr(self, "settings"): + self.settings = settings.settings + i18n_sett = context.window_manager.i18n_update_svn_settings + # 'DEFAULT' and en_US are always valid, fully-translated "languages"! + stats = {"DEFAULT": 1.0, "en_US": 1.0} + + 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 + print("Processing {} language ({}).".format(lng.name, lng.uid)) + po = i18n_utils.I18nMessages(uid=lng.uid, kind='PO', src=lng.po_path, settings=self.settings) + print("Cleaned up {} commented messages.".format(po.clean_commented())) + errs = po.check(fix=True) + if errs: + print("Errors in this po, solved as best as possible!") + print("\t" + "\n\t".join(errs)) + if lng.uid in self.settings.IMPORT_LANGUAGES_RTL: + po.write(kind="PO", dest=lng.po_path_trunk[:-3] + "_raw.po") + po.rtl_process() + po.write(kind="PO", dest=lng.po_path_trunk) + po.write(kind="MO", dest=lng.mo_path_trunk) + po.update_info() + stats[lng.uid] = po.nbr_trans_msgs / po.nbr_msgs + print("\n") + + print("Generating languages' menu...") + # First complete our statistics by checking po files we did not touch this time! + po_to_uid = {os.path.basename(lng.po_path): lng.uid for lng in i18n_sett.langs} + for po_path in os.listdir(self.settings.TRUNK_PO_DIR): + uid = po_to_uid.get(po_path, None) + po_path = os.path.join(self.settings.TRUNK_PO_DIR, po_path) + if uid and uid not in stats: + po = i18n_utils.I18nMessages(uid=uid, kind='PO', src=po_path, settings=self.settings) + stats[uid] = po.nbr_trans_msgs / po.nbr_msgs + languages_menu_utils.gen_menu_file(stats, self.settings) + + return {'FINISHED'} diff --git a/ui_translate/utils.py b/ui_translate/utils.py deleted file mode 100644 index b852f641d..000000000 --- a/ui_translate/utils.py +++ /dev/null @@ -1,191 +0,0 @@ -# ##### 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> - -#from bl_i18n_utils import utils as i18n_utils -from bl_i18n_utils import settings - -import os - - -# module-level cache, as parsing po files takes a few seconds... -# Keys are po file paths, data are the results of i18n_utils.parse_messages(). -WORK_CACHE = {} - -# Same as in BLF_translation.h -BLF_I18NCONTEXT_DEFAULT = "" - - -# Num buttons report their label with a trailing ': '... -NUM_BUTTON_SUFFIX = ": " - - -# Mo root datapath. -MO_PATH_ROOT = "locale" - -# Mo path generator for a given language. -MO_PATH_TEMPLATE = os.path.join(MO_PATH_ROOT, "{}", "LC_MESSAGES") - -# Mo filename. -MO_FILENAME = "blender.mo" - - -def bpy_path(rstruct, rprop, renum): - src = src_rna = src_enum = "" - if rstruct: - if rprop: - src = src_rna = ".".join((rstruct, rprop)) - if renum: - src = src_enum = "{}.{}:'{}'".format(rstruct, rprop, renum) - else: - src = src_rna = rstruct - return src, src_rna, src_enum - - -def find_best_msgs_matches(obj, cache_key, msgmap, msgs, state, rna_ctxt, rstruct, rprop, renum): - comm_prfx = settings.COMMENT_PREFIX_SOURCE + "bpy.types." - - # Build helper mappings. - # XXX We do not update this cache when editing a translation, as it would - # prevent the same msgid/msgstr to be find again. - # We only invalidate the cache once new po/mo have been generated! - if cache_key in WORK_CACHE: - src_to_msg, ctxt_to_msg, msgid_to_msg, msgstr_to_msg = WORK_CACHE[cache_key] - else: - src_to_msg = {} - ctxt_to_msg = {} - msgid_to_msg = {} - msgstr_to_msg = {} - for key, val in msgs.items(): - ctxt, msgid = key - if key in state["comm_msg"]: - continue - ctxt_to_msg.setdefault(ctxt, set()).add(key) - msgid_to_msg.setdefault(msgid, set()).add(key) - msgstr_to_msg.setdefault("".join(val["msgstr_lines"]), set()).add(key) - for comm in val["comment_lines"]: - if comm.startswith(comm_prfx): - comm = comm[len(comm_prfx):] - src_to_msg.setdefault(comm, set()).add(key) - WORK_CACHE[cache_key] = (src_to_msg, ctxt_to_msg, msgid_to_msg, msgstr_to_msg) - -# print(len(src_to_msg), len(ctxt_to_msg), len(msgid_to_msg), len(msgstr_to_msg)) - - # Build RNA key. - src, src_rna, src_enum = bpy_path(rstruct, rprop, renum) - print("src: ", src_rna, src_enum) - - # Labels. - elbl = getattr(obj, msgmap["enum_label"]["msgstr"]) - print("enum label: %r" % elbl) - if elbl: - # Enum items' labels have no i18n context... - k = ctxt_to_msg[BLF_I18NCONTEXT_DEFAULT].copy() - if elbl in msgid_to_msg: - k &= msgid_to_msg[elbl] - elif elbl in msgstr_to_msg: - k &= msgstr_to_msg[elbl] - else: - k = set() - # We assume if we already have only one key, it's the good one! - if len(k) > 1 and src_enum in src_to_msg: - k &= src_to_msg[src_enum] - msgmap["enum_label"]["key"] = k - rlbl = getattr(obj, msgmap["rna_label"]["msgstr"]) - print("rna label: %r" % rlbl, rlbl in msgid_to_msg, rlbl in msgstr_to_msg) - if rlbl: - k = ctxt_to_msg[rna_ctxt].copy() - if k and rlbl in msgid_to_msg: - k &= msgid_to_msg[rlbl] - elif k and rlbl in msgstr_to_msg: - k &= msgstr_to_msg[rlbl] - else: - k = set() - # We assume if we already have only one key, it's the good one! - if len(k) > 1 and src_rna in src_to_msg: - k &= src_to_msg[src_rna] - msgmap["rna_label"]["key"] = k - blbl = getattr(obj, msgmap["but_label"]["msgstr"]) - blbls = [blbl] - if blbl.endswith(NUM_BUTTON_SUFFIX): - # Num buttons report their label with a trailing ': '... - blbls.append(blbl[:-len(NUM_BUTTON_SUFFIX)]) - print("button label: %r" % blbl) - if blbl and elbl not in blbls and (rlbl not in blbls or rna_ctxt != BLF_I18NCONTEXT_DEFAULT): - # Always Default context for button label :/ - k = ctxt_to_msg[BLF_I18NCONTEXT_DEFAULT].copy() - found = False - for bl in blbls: - if bl in msgid_to_msg: - k &= msgid_to_msg[bl] - found = True - break - elif bl in msgstr_to_msg: - k &= msgstr_to_msg[bl] - found = True - break - if not found: - k = set() - # XXX No need to check against RNA path here, if blabel is different - # from rlabel, should not match anyway! - msgmap["but_label"]["key"] = k - - # Tips (they never have a specific context). - etip = getattr(obj, msgmap["enum_tip"]["msgstr"]) - print("enum tip: %r" % etip) - if etip: - k = ctxt_to_msg[BLF_I18NCONTEXT_DEFAULT].copy() - if etip in msgid_to_msg: - k &= msgid_to_msg[etip] - elif etip in msgstr_to_msg: - k &= msgstr_to_msg[etip] - else: - k = set() - # We assume if we already have only one key, it's the good one! - if len(k) > 1 and src_enum in src_to_msg: - k &= src_to_msg[src_enum] - msgmap["enum_tip"]["key"] = k - rtip = getattr(obj, msgmap["rna_tip"]["msgstr"]) - print("rna tip: %r" % rtip) - if rtip: - k = ctxt_to_msg[BLF_I18NCONTEXT_DEFAULT].copy() - if k and rtip in msgid_to_msg: - k &= msgid_to_msg[rtip] - elif k and rtip in msgstr_to_msg: - k &= msgstr_to_msg[rtip] - else: - k = set() - # We assume if we already have only one key, it's the good one! - if len(k) > 1 and src_rna in src_to_msg: - k &= src_to_msg[src_rna] - msgmap["rna_tip"]["key"] = k - print(k) - btip = getattr(obj, msgmap["but_tip"]["msgstr"]) - print("button tip: %r" % btip) - if btip and btip not in {rtip, etip}: - k = ctxt_to_msg[BLF_I18NCONTEXT_DEFAULT].copy() - if btip in msgid_to_msg: - k &= msgid_to_msg[btip] - elif btip in msgstr_to_msg: - k &= msgstr_to_msg[btip] - else: - k = set() - # XXX No need to check against RNA path here, if btip is different - # from rtip, should not match anyway! - msgmap["but_tip"]["key"] = k -- GitLab