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