Moved the flask-babel support into its own package.

Sat, 21 Nov 2020 20:37:54 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 21 Nov 2020 20:37:54 +0100
changeset 17
f31df56510a1
parent 16
dd3f6bfb85f7
child 18
d76a0939be6a

Moved the flask-babel support into its own package.

PluginFlask.e4p file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelDetector.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/FlaskBabelDetector.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/PyBabelCommandDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/PyBabelProjectExtension.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskBabelExtension/__init__.py file | annotate | diff | comparison | revisions
ProjectFlask/Project.py file | annotate | diff | comparison | revisions
ProjectFlask/PyBabelCommandDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/PyBabelConfigDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/PyBabelConfigDialog.ui file | annotate | diff | comparison | revisions
--- a/PluginFlask.e4p	Sat Nov 21 17:50:57 2020 +0100
+++ b/PluginFlask.e4p	Sat Nov 21 20:37:54 2020 +0100
@@ -18,11 +18,13 @@
     <Source>ProjectFlask/AnsiTools.py</Source>
     <Source>ProjectFlask/ConfigurationPage/FlaskPage.py</Source>
     <Source>ProjectFlask/ConfigurationPage/__init__.py</Source>
-    <Source>ProjectFlask/FlaskBabelDetector.py</Source>
+    <Source>ProjectFlask/FlaskBabelExtension/FlaskBabelDetector.py</Source>
+    <Source>ProjectFlask/FlaskBabelExtension/PyBabelCommandDialog.py</Source>
+    <Source>ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.py</Source>
+    <Source>ProjectFlask/FlaskBabelExtension/PyBabelProjectExtension.py</Source>
+    <Source>ProjectFlask/FlaskBabelExtension/__init__.py</Source>
     <Source>ProjectFlask/FlaskCommandDialog.py</Source>
     <Source>ProjectFlask/Project.py</Source>
-    <Source>ProjectFlask/PyBabelCommandDialog.py</Source>
-    <Source>ProjectFlask/PyBabelConfigDialog.py</Source>
     <Source>ProjectFlask/RoutesDialog.py</Source>
     <Source>ProjectFlask/RunServerDialog.py</Source>
     <Source>ProjectFlask/ServerStartOptionsDialog.py</Source>
@@ -31,8 +33,8 @@
   </Sources>
   <Forms>
     <Form>ProjectFlask/ConfigurationPage/FlaskPage.ui</Form>
+    <Form>ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui</Form>
     <Form>ProjectFlask/FlaskCommandDialog.ui</Form>
-    <Form>ProjectFlask/PyBabelConfigDialog.ui</Form>
     <Form>ProjectFlask/RoutesDialog.ui</Form>
     <Form>ProjectFlask/RunServerDialog.ui</Form>
     <Form>ProjectFlask/ServerStartOptionsDialog.ui</Form>
--- a/ProjectFlask/FlaskBabelDetector.py	Sat Nov 21 17:50:57 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module to check for the presence of 'flask-babel' by importing it.
-"""
-
-import sys
-
-if __name__ == "__main__":
-    try:
-        import flask_babel      # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
-        ret = 0
-    except ImportError:
-        ret = 1
-    
-    sys.exit(ret)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/FlaskBabelDetector.py	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module to check for the presence of 'flask-babel' by importing it.
+"""
+
+import sys
+
+if __name__ == "__main__":
+    try:
+        import flask_babel      # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
+        ret = 0
+    except ImportError:
+        ret = 1
+    
+    sys.exit(ret)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/PyBabelCommandDialog.py	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to run a flask command and show its output.
+"""
+
+from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton
+
+from E5Gui import E5MessageBox
+
+from ..Ui_FlaskCommandDialog import Ui_FlaskCommandDialog
+
+
+class PyBabelCommandDialog(QDialog, Ui_FlaskCommandDialog):
+    """
+    Class implementing a dialog to run a flask command and show its output.
+    """
+    def __init__(self, project, title="", msgSuccess="", msgError="",
+                 parent=None):
+        """
+        Constructor
+        
+        @param project reference to the project object
+        @type Project
+        @param title window title of the dialog
+        @type str
+        @param msgSuccess success message to be shown
+        @type str
+        @param msgError message to be shown on error
+        @type str
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(PyBabelCommandDialog, self).__init__(parent)
+        self.setupUi(self)
+        
+        if title:
+            self.setWindowTitle(title)
+        
+        self.__project = project
+        self.__successMessage = msgSuccess
+        self.__errorMessage = msgError
+        
+        self.__process = None
+        self.__argsLists = []
+        self.__workdir = ""
+        
+        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
+        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
+    
+    def startCommand(self, command, args, workdir, clearOutput=True):
+        """
+        Public method to start a pybabel command and show its output.
+        
+        @param command pybabel command to be run
+        @type str
+        @param args list of command line arguments for the command
+        @type list of str
+        @param workdir working directory for the command
+        @type str
+        @param clearOutput flag indicating to clear the output
+        @type bool
+        @return flag indicating a successful start
+        @rtype bool
+        """
+        babelCommand = self.__project.getBabelCommand()
+        
+        self.__process = QProcess()
+        self.__process.setWorkingDirectory(workdir)
+        self.__process.setProcessChannelMode(QProcess.MergedChannels)
+        
+        self.__process.readyReadStandardOutput.connect(self.__readStdOut)
+        self.__process.finished.connect(self.__processFinished)
+        
+        if clearOutput:
+            self.outputEdit.clear()
+        
+        babelArgs = [command]
+        if args:
+            babelArgs += args
+        
+        self.__process.start(babelCommand, babelArgs)
+        ok = self.__process.waitForStarted(10000)
+        if not ok:
+            E5MessageBox.critical(
+                None,
+                self.tr("Execute PyBabel Command"),
+                self.tr("""The pybabel process could not be started."""))
+        else:
+            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
+            self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
+            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
+            self.buttonBox.button(QDialogButtonBox.Cancel).setFocus(
+                Qt.OtherFocusReason)
+        
+        return ok
+    
+    def startBatchCommand(self, argsLists, workdir):
+        """
+        Public method to start a pybabel command repeatedly with a list of
+        arguments and show the output.
+        
+        @param argsLists list of command line arguments for the batch commands
+        @type list of lists of str
+        @param workdir working directory for the command
+        @type str
+        @return flag indicating a successful start of the first process
+        @rtype bool
+        """
+        self.__argsLists = argsLists[:]
+        self.__workdir = workdir
+        
+        # start the first process
+        args = self.__argsLists.pop(0)
+        res = self.startCommand(args[0], args[1:], workdir)
+        if not res:
+            self.__argsLists = []
+        
+        return res
+    
+    def closeEvent(self, evt):
+        """
+        Protected method handling the close event of the dialog.
+        
+        @param evt reference to the close event object
+        @type QCloseEvent
+        """
+        self.__argsLists = []
+        self.__cancelProcess()
+        evt.accept()
+    
+    @pyqtSlot()
+    def __readStdOut(self):
+        """
+        Private slot to add the server process output to the output pane.
+        """
+        if self.__process is not None:
+            out = str(self.__process.readAllStandardOutput(), "utf-8")
+            self.outputEdit.insertPlainText(out)
+    
+    def __processFinished(self, exitCode, exitStatus):
+        """
+        Private slot connected to the finished signal.
+        
+        @param exitCode exit code of the process
+        @type int
+        @param exitStatus exit status of the process
+        @type QProcess.ExitStatus
+        """
+        normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0)
+        self.__cancelProcess()
+        
+        if self.__argsLists:
+            args = self.__argsLists.pop(0)
+            self.startCommand(args[0], args[1:], self.__workdir,
+                              clearOutput=False)
+            return
+        
+        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
+        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
+            Qt.OtherFocusReason)
+        
+        if normal and self.__successMessage:
+            self.outputEdit.insertPlainText(self.__successMessage)
+        elif not normal and self.__errorMessage:
+            self.outputEdit.insertPlainText(self.__errorMessage)
+    
+    @pyqtSlot()
+    def __cancelProcess(self):
+        """
+        Private slot to terminate the current process.
+        """
+        if (
+            self.__process is not None and
+            self.__process.state() != QProcess.NotRunning
+        ):
+            self.__process.terminate()
+            QTimer.singleShot(2000, self.__process.kill)
+            self.__process.waitForFinished(3000)
+        
+        self.__process = None
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot handling presses of the button box buttons.
+        
+        @param button reference to the button been clicked
+        @type QAbstractButton
+        """
+        if button is self.buttonBox.button(QDialogButtonBox.Close):
+            self.close()
+        elif button is self.buttonBox.button(QDialogButtonBox.Cancel):
+            self.__argsLists = []
+            self.__cancelProcess()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.py	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to edit the PyBabel configuration.
+"""
+
+import os
+
+from PyQt5.QtCore import pyqtSlot, Qt
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox
+
+from E5Gui.E5PathPicker import E5PathPickerModes
+from E5Gui.E5Application import e5App
+
+from .Ui_PyBabelConfigDialog import Ui_PyBabelConfigDialog
+
+
+class PyBabelConfigDialog(QDialog, Ui_PyBabelConfigDialog):
+    """
+    Class implementing a dialog to edit the PyBabel configuration.
+    """
+    def __init__(self, configuration, parent=None):
+        """
+        Constructor
+        
+        @param configuration current pybabel configuration
+        @type dict
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(PyBabelConfigDialog, self).__init__(parent)
+        self.setupUi(self)
+        
+        self.__e5project = e5App().getObject("Project")
+        
+        self.configFilePicker.setMode(
+            E5PathPickerModes.SaveFileEnsureExtensionMode)
+        self.configFilePicker.setFilters(self.tr(
+            "Configuration Files (*.cfg);;"
+            "All Files (*)"
+        ))
+        self.configFilePicker.setDefaultDirectory(
+            self.__e5project.getProjectPath())
+        
+        self.translationsDirectoryPicker.setMode(
+            E5PathPickerModes.DirectoryMode)
+        self.translationsDirectoryPicker.setDefaultDirectory(
+            self.__e5project.getProjectPath())
+        
+        self.catalogFilePicker.setMode(
+            E5PathPickerModes.SaveFileEnsureExtensionMode)
+        self.catalogFilePicker.setFilters(self.tr(
+            "Message Catalog Files (*.pot);;"
+            "All Files (*)"
+        ))
+        self.catalogFilePicker.setDefaultDirectory(
+            self.__e5project.getProjectPath())
+        
+        self.configFilePicker.setFocus(Qt.OtherFocusReason)
+        
+        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
+        
+        if "configFile" in configuration:
+            self.configFilePicker.setText(
+                self.__e5project.getAbsoluteUniversalPath(
+                    configuration["configFile"]))
+        if "translationsDirectory" in configuration:
+            self.translationsDirectoryPicker.setText(
+                self.__e5project.getAbsoluteUniversalPath(
+                    configuration["translationsDirectory"]))
+        if "domain" in configuration:
+            self.domainEdit.setText(configuration["domain"])
+        if "catalogFile" in configuration:
+            self.catalogFilePicker.setText(
+                self.__e5project.getAbsoluteUniversalPath(
+                    configuration["catalogFile"]))
+        if "markersList" in configuration:
+            self.markersEdit.setText(" ".join(configuration["markersList"]))
+        
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+    
+    def getConfiguration(self):
+        """
+        Public method to get the entered configuration data.
+        
+        @return pybabel configuration
+        @rtype dict
+        """
+        configuration = {
+            "configFile": self.__e5project.getRelativeUniversalPath(
+                self.configFilePicker.text()),
+            "translationsDirectory": self.__e5project.getRelativeUniversalPath(
+                self.translationsDirectoryPicker.text()),
+        }
+        
+        domain = self.domainEdit.text()
+        if domain:
+            configuration["domain"] = domain
+        else:
+            configuration["domain"] = "messages"
+        
+        catalogFile = self.catalogFilePicker.text()
+        if not catalogFile:
+            # use a default name made of translations dir and domain
+            catalogFile = os.path.join(
+                configuration["translationsDirectory"],
+                "{0}.pot".format(configuration["domain"]))
+        configuration["catalogFile"] = (
+            self.__e5project.getRelativeUniversalPath(catalogFile)
+        )
+        
+        if self.markersEdit.text():
+            configuration["markersList"] = self.markersEdit.text().split()
+        
+        return configuration
+    
+    def __updateOK(self):
+        """
+        Private method to update the status of the OK button.
+        """
+        enable = (
+            bool(self.configFilePicker.text()) and
+            bool(self.translationsDirectoryPicker.text())
+        )
+        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable)
+    
+    def __updateCatalogPicker(self):
+        """
+        Private method to update the contents of the catalog picker.
+        """
+        translationsDirectory = self.translationsDirectoryPicker.text()
+        domain = self.domainEdit.text()
+        self.catalogFilePicker.setText(os.path.join(
+            translationsDirectory, "{0}.pot".format(domain)))
+    
+    @pyqtSlot(str)
+    def on_configFilePicker_textChanged(self, txt):
+        """
+        Private slot to handle a change of the configuration file name.
+        
+        @param txt configuration file name
+        @type str
+        """
+        self.__updateOK()
+    
+    @pyqtSlot(str)
+    def on_translationsDirectoryPicker_textChanged(self, txt):
+        """
+        Private slot to handle a change of the catalog file name.
+        
+        @param txt configuration file name
+        @type str
+        """
+        self.__updateOK()
+        self.__updateCatalogPicker()
+    
+    @pyqtSlot(str)
+    def on_domainEdit_textChanged(self, txt):
+        """
+        Private slot to handle a change of the translations domain.
+        
+        @param txt entered translations domain
+        @type str
+        """
+        self.__updateCatalogPicker()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PyBabelConfigDialog</class>
+ <widget class="QDialog" name="PyBabelConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>150</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>PyBabel Configuration</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Configuration File:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="E5PathPicker" name="configFilePicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the name of the PyBabel configuration file</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label_4">
+     <property name="text">
+      <string>Translations Directory:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="E5PathPicker" name="translationsDirectoryPicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the name of the directory containing the translations</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_5">
+     <property name="text">
+      <string>Domain:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QLineEdit" name="domainEdit">
+     <property name="toolTip">
+      <string>Enter the name of the translations domain (leave empty for default)</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Message Catalog:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="E5PathPicker" name="catalogFilePicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the name of the message catalog file</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Translation Markers:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="1">
+    <widget class="QLineEdit" name="markersEdit">
+     <property name="toolTip">
+      <string>Enter the translation markers separated by space (_ is included by default)</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>E5PathPicker</class>
+   <extends>QWidget</extends>
+   <header>E5Gui/E5PathPicker.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>configFilePicker</tabstop>
+  <tabstop>translationsDirectoryPicker</tabstop>
+  <tabstop>domainEdit</tabstop>
+  <tabstop>catalogFilePicker</tabstop>
+  <tabstop>markersEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>PyBabelConfigDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>PyBabelConfigDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/PyBabelProjectExtension.py	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,578 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the project support for flask-babel.
+"""
+
+import os
+import re
+
+from PyQt5.QtCore import pyqtSlot, QObject, QProcess
+from PyQt5.QtWidgets import QDialog
+
+from E5Gui import E5MessageBox
+from E5Gui.E5Application import e5App
+
+from .PyBabelCommandDialog import PyBabelCommandDialog
+
+import Utilities
+
+
+class PyBabelProject(QObject):
+    """
+    Class implementing the Flask project support.
+    """
+    def __init__(self, plugin, project, parent=None):
+        """
+        Constructor
+        
+        @param plugin reference to the plugin object
+        @type ProjectFlaskPlugin
+        @param project reference to the project object
+        @type Project
+        @param parent parent
+        @type QObject
+        """
+        super(PyBabelProject, self).__init__(parent)
+        
+        self.__plugin = plugin
+        self.__project = project
+        
+        self.__e5project = e5App().getObject("Project")
+        self.__virtualEnvManager = e5App().getObject("VirtualEnvManager")
+        
+        self.__hooksInstalled = False
+    
+    def registerOpenHook(self):
+        """
+        Public method to register the open hook to open a translations file
+        in a translations editor.
+        """
+        if self.__hooksInstalled:
+            editor = self.__plugin.getPreferences("TranslationsEditor")
+            if editor:
+                self.__translationsBrowser.addHookMethodAndMenuEntry(
+                    "open", self.openPOEditor,
+                    self.tr("Open with {0}").format(
+                        os.path.basename(editor)))
+            else:
+                self.__translationsBrowser.removeHookMethod("open")
+    
+    def projectOpenedHooks(self):
+        """
+        Public method to add our hook methods.
+        """
+        if self.__project.hasCapability("pybabel"):
+            self.__e5project.projectLanguageAddedByCode.connect(
+                self.__projectLanguageAdded)
+            self.__translationsBrowser = (
+                e5App().getObject("ProjectBrowser")
+                .getProjectBrowser("translations"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "extractMessages", self.extractMessages,
+                self.tr("Extract Messages"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "releaseAll", self.compileCatalogs,
+                self.tr("Compile All Catalogs"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "releaseSelected", self.compileSelectedCatalogs,
+                self.tr("Compile Selected Catalogs"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "generateAll", self.updateCatalogs,
+                self.tr("Update All Catalogs"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "generateAllWithObsolete", self.updateCatalogsObsolete,
+                self.tr("Update All Catalogs (with obsolete)"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "generateSelected", self.updateSelectedCatalogs,
+                self.tr("Update Selected Catalogs"))
+            self.__translationsBrowser.addHookMethodAndMenuEntry(
+                "generateSelectedWithObsolete",
+                self.updateSelectedCatalogsObsolete,
+                self.tr("Update Selected Catalogs (with obsolete)"))
+            
+            self.__hooksInstalled = True
+        
+            self.registerOpenHook()
+    
+    def projectClosedHooks(self):
+        """
+        Public method to remove our hook methods.
+        """
+        if self.__hooksInstalled:
+            self.__e5project.projectLanguageAddedByCode.disconnect(
+                self.__projectLanguageAdded)
+            self.__translationsBrowser.removeHookMethod(
+                "extractMessages")
+            self.__translationsBrowser.removeHookMethod(
+                "releaseAll")
+            self.__translationsBrowser.removeHookMethod(
+                "releaseSelected")
+            self.__translationsBrowser.removeHookMethod(
+                "generateAll")
+            self.__translationsBrowser.removeHookMethod(
+                "generateAllWithObsolete")
+            self.__translationsBrowser.removeHookMethod(
+                "generateSelected")
+            self.__translationsBrowser.removeHookMethod(
+                "generateSelectedWithObsolete")
+            self.__translationsBrowser.removeHookMethod(
+                "open")
+            self.__translationsBrowser = None
+        
+        self.__hooksInstalled = False
+    
+    def determineCapability(self):
+        """
+        Public method to determine the availability of flask-babel.
+        """
+        self.__project.setCapability("pybabel", self.flaskBabelAvailable())
+        
+    ##################################################################
+    ## slots and methods below implement general functionality
+    ##################################################################
+    
+    def getBabelCommand(self):
+        """
+        Public method to build the Babel command.
+        
+        @return full pybabel command
+        @rtype str
+        """
+        return self.__project.getFullCommand("pybabel")
+    
+    ##################################################################
+    ## slots and methods below implement i18n and l10n support
+    ##################################################################
+    
+    def flaskBabelAvailable(self):
+        """
+        Public method to check, if the 'flask-babel' package is available.
+        
+        @return flag indicating the availability of 'flask-babel'
+        @rtype bool
+        """
+        venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3")
+        interpreter = self.__virtualEnvManager.getVirtualenvInterpreter(
+            venvName)
+        if interpreter and Utilities.isinpath(interpreter):
+            detector = os.path.join(
+                os.path.dirname(__file__), "FlaskBabelDetector.py")
+            proc = QProcess()
+            proc.setProcessChannelMode(QProcess.MergedChannels)
+            proc.start(interpreter, [detector])
+            finished = proc.waitForFinished(30000)
+            if finished and proc.exitCode() == 0:
+                return True
+        
+        return False
+    
+    @pyqtSlot()
+    def configurePyBabel(self):
+        """
+        Public slot to show a dialog to edit the pybabel configuration.
+        """
+        from .PyBabelConfigDialog import PyBabelConfigDialog
+        
+        config = self.__project.getData("pybabel", "")
+        dlg = PyBabelConfigDialog(config)
+        if dlg.exec() == QDialog.Accepted:
+            config = dlg.getConfiguration()
+            self.__project.setData("pybabel", "", config)
+            
+            self.__e5project.setTranslationPattern(os.path.join(
+                config["translationsDirectory"], "%language%", "LC_MESSAGES",
+                "{0}.po".format(config["domain"])
+            ))
+            self.__e5project.setDirty(True)
+            
+            cfgFileName = self.__e5project.getAbsoluteUniversalPath(
+                config["configFile"])
+            if not os.path.exists(cfgFileName):
+                self.__createBabelCfg(cfgFileName)
+    
+    def __ensurePybabelConfigured(self):
+        """
+        Private method to ensure, that PyBabel has been configured.
+        
+        @return flag indicating successful configuration
+        @rtype bool
+        """
+        config = self.__project.getData("pybabel", "")
+        if not config:
+            self.__configurePybabel()
+            return True
+        
+        configFileName = self.__project.getData("pybabel", "configFile")
+        if configFileName:
+            cfgFileName = self.__e5project.getAbsoluteUniversalPath(
+                configFileName)
+            if os.path.exists(cfgFileName):
+                return True
+            else:
+                return self.__createBabelCfg(cfgFileName)
+        
+        return False
+    
+    def __createBabelCfg(self, configFile):
+        """
+        Private method to create a template pybabel configuration file.
+        
+        @param configFile name of the configuration file to be created
+        @type str
+        @return flag indicating successful configuration file creation
+        @rtype bool
+        """
+        _, app = self.getApplication()
+        if app.endswith(".py"):
+            template = (
+                "[python: {0}]\n"
+                "[jinja2: templates/**.html]\n"
+                "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
+            )
+        else:
+            template = (
+                "[python: {0}/**.py]\n"
+                "[jinja2: {0}/templates/**.html]\n"
+                "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
+            )
+        try:
+            with open(configFile, "w") as f:
+                f.write(template.format(app))
+            self.__e5project.appendFile(configFile)
+            E5MessageBox.information(
+                None,
+                self.tr("Generate PyBabel Configuration File"),
+                self.tr("""The PyBabel configuration file was created."""
+                        """ Please edit it to adjust the entries as"""
+                        """ required.""")
+            )
+            return True
+        except EnvironmentError as err:
+            E5MessageBox.warning(
+                None,
+                self.tr("Generate PyBabel Configuration File"),
+                self.tr("""<p>The PyBabel Configuration File could not be"""
+                        """ generated.</p><p>Reason: {0}</p>""")
+                .format(str(err))
+            )
+            return False
+    
+    def __getLocale(self, filename):
+        """
+        Private method to extract the locale out of a file name.
+        
+        @param filename name of the file used for extraction
+        @type str
+        @return extracted locale
+        @rtype str or None
+        """
+        if self.__e5project.getTranslationPattern():
+            filename = os.path.splitext(filename)[0] + ".po"
+            
+            # On Windows, path typically contains backslashes. This leads
+            # to an invalid search pattern '...\(' because the opening bracket
+            # will be escaped.
+            pattern = self.__e5project.getTranslationPattern()
+            pattern = os.path.normpath(pattern)
+            pattern = pattern.replace("%language%", "(.*?)")
+            pattern = pattern.replace('\\', '\\\\')
+            match = re.search(pattern, filename)
+            if match is not None:
+                return match.group(1)
+        
+        return None
+    
+    def openPOEditor(self, poFile):
+        """
+        Public method to edit the given file in an external .po editor.
+        
+        @param poFile name of the .po file
+        @type str
+        """
+        editor = self.__plugin.getPreferences("TranslationsEditor")
+        if poFile.endswith(".po") and editor:
+            workdir = self.__project.getApplication()[0]
+            started, pid = QProcess.startDetached(editor, [poFile], workdir)
+            if not started:
+                E5MessageBox.critical(
+                    None,
+                    self.tr('Process Generation Error'),
+                    self.tr('The translations editor process ({0}) could'
+                            ' not be started.').format(
+                        os.path.basename(editor)))
+    
+    def extractMessages(self):
+        """
+        Public method to extract the messages catalog template file.
+        """
+        title = self.tr("Extract messages")
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "catalogFile"))
+            
+            try:
+                potFilePath = os.path.dirname(potFile)
+                os.makedirs(potFilePath)
+            except EnvironmentError:
+                pass
+            
+            args = [
+                "-F",
+                os.path.relpath(
+                    self.__e5project.getAbsoluteUniversalPath(
+                        self.__project.getData("pybabel", "configFile")),
+                    workdir
+                )
+            ]
+            if self.__project.getData("pybabel", "markersList"):
+                for marker in self.__project.getData("pybabel", "markersList"):
+                    args += ["-k", marker]
+            args += [
+                "-o",
+                os.path.relpath(potFile, workdir),
+                "."
+            ]
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr("\nMessages extracted successfully.")
+            )
+            res = dlg.startCommand("extract", args, workdir)
+            if res:
+                dlg.exec()
+                self.__e5project.appendFile(potFile)
+    
+    def __projectLanguageAdded(self, code):
+        """
+        Private slot handling the addition of a new language.
+        
+        @param code language code of the new language
+        @type str
+        """
+        title = self.tr(
+            "Initializing message catalog for '{0}'").format(code)
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            langFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__e5project.getTranslationPattern().replace(
+                    "%language%", code))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "catalogFile"))
+            
+            args = [
+                "--domain={0}".format(
+                    self.__project.getData("pybabel", "domain")),
+                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
+                "--output-file={0}".format(os.path.relpath(langFile, workdir)),
+                "--locale={0}".format(code),
+            ]
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr(
+                    "\nMessage catalog initialized successfully.")
+            )
+            res = dlg.startCommand("init", args, workdir)
+            if res:
+                dlg.exec()
+                
+                self.__e5project.appendFile(langFile)
+    
+    def compileCatalogs(self, filenames):
+        """
+        Public method to compile the message catalogs.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        title = self.tr("Compiling message catalogs")
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "translationsDirectory"))
+            
+            args = [
+                "--domain={0}".format(
+                    self.__project.getData("pybabel", "domain")),
+                "--directory={0}".format(
+                    os.path.relpath(translationsDirectory, workdir)),
+                "--use-fuzzy",
+                "--statistics",
+            ]
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
+            )
+            res = dlg.startCommand("compile", args, workdir)
+            if res:
+                dlg.exec()
+            
+                for entry in os.walk(translationsDirectory):
+                    for fileName in entry[2]:
+                        fullName = os.path.join(entry[0], fileName)
+                        if fullName.endswith('.mo'):
+                            self.__e5project.appendFile(fullName)
+    
+    def compileSelectedCatalogs(self, filenames):
+        """
+        Public method to update the message catalogs.
+        
+        @param filenames list of file names
+        @type list of str
+        """
+        title = self.tr("Compiling message catalogs")
+        
+        locales = {self.__getLocale(f) for f in filenames}
+        
+        if len(locales) == 0:
+            E5MessageBox.warning(
+                self.__ui,
+                title,
+                self.tr('No locales detected. Aborting...'))
+            return
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "translationsDirectory"))
+            
+            argsList = []
+            for loc in locales:
+                argsList.append([
+                    "compile",
+                    "--domain={0}".format(
+                        self.__project.getData("pybabel", "domain")),
+                    "--directory={0}".format(
+                        os.path.relpath(translationsDirectory, workdir)),
+                    "--use-fuzzy",
+                    "--statistics",
+                    "--locale={0}".format(loc),
+                ])
+            
+            dlg = PyBabelCommandDialog(
+                self, title=title,
+                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
+            )
+            res = dlg.startBatchCommand(argsList, workdir)
+            if res:
+                dlg.exec()
+            
+                for entry in os.walk(translationsDirectory):
+                    for fileName in entry[2]:
+                        fullName = os.path.join(entry[0], fileName)
+                        if fullName.endswith('.mo'):
+                            self.__e5project.appendFile(fullName)
+    
+    def updateCatalogs(self, filenames, withObsolete=False):
+        """
+        Public method to update the message catalogs.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        @param withObsolete flag indicating to keep obsolete translations
+        @type bool
+        """
+        title = self.tr("Updating message catalogs")
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "translationsDirectory"))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "catalogFile"))
+            
+            args = [
+                "--domain={0}".format(
+                    self.__project.getData("pybabel", "domain")),
+                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
+                "--output-dir={0}".format(
+                    os.path.relpath(translationsDirectory, workdir)),
+            ]
+            if not withObsolete:
+                args.append("--ignore-obsolete")
+            
+            dlg = PyBabelCommandDialog(
+                self, title,
+                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
+            )
+            res = dlg.startCommand("update", args, workdir)
+            if res:
+                dlg.exec()
+    
+    def updateCatalogsObsolete(self, filenames):
+        """
+        Public method to update the message catalogs keeping obsolete
+        translations.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        self.updateCatalogs(filenames, withObsolete=True)
+    
+    def updateSelectedCatalogs(self, filenames, withObsolete=False):
+        """
+        Public method to update the selected message catalogs.
+        
+        @param filenames list of filenames
+        @type list of str
+        @param withObsolete flag indicating to keep obsolete translations
+        @type bool
+        """
+        title = self.tr("Updating message catalogs")
+        
+        locales = {self.__getLocale(f) for f in filenames}
+        
+        if len(locales) == 0:
+            E5MessageBox.warning(
+                self.__ui,
+                title,
+                self.tr('No locales detected. Aborting...'))
+            return
+        
+        if self.__ensurePybabelConfigured():
+            workdir = self.__project.getApplication()[0]
+            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "translationsDirectory"))
+            potFile = self.__e5project.getAbsoluteUniversalPath(
+                self.__project.getData("pybabel", "catalogFile"))
+            argsList = []
+            for loc in locales:
+                args = [
+                    "update",
+                    "--domain={0}".format(
+                        self.__project.getData("pybabel", "domain")),
+                    "--input-file={0}".format(
+                        os.path.relpath(potFile, workdir)),
+                    "--output-dir={0}".format(
+                        os.path.relpath(translationsDirectory, workdir)),
+                    "--locale={0}".format(loc),
+                ]
+                if not withObsolete:
+                    args.append("--ignore-obsolete")
+                argsList.append(args)
+            
+            dlg = PyBabelCommandDialog(
+                self, title=title,
+                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
+            )
+            res = dlg.startBatchCommand(argsList, workdir)
+            if res:
+                dlg.exec()
+    
+    def updateSelectedCatalogsObsolete(self, filenames):
+        """
+        Public method to update the message catalogs keeping obsolete
+        translations.
+        
+        @param filenames list of filenames (not used)
+        @type list of str
+        """
+        self.updateSelectedCatalogs(filenames, withObsolete=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskBabelExtension/__init__.py	Sat Nov 21 20:37:54 2020 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package containing support for the flask-babel extension.
+"""
--- a/ProjectFlask/Project.py	Sat Nov 21 17:50:57 2020 +0100
+++ b/ProjectFlask/Project.py	Sat Nov 21 20:37:54 2020 +0100
@@ -8,12 +8,11 @@
 """
 
 import os
-import re
 
 from PyQt5.QtCore import (
     pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer
 )
-from PyQt5.QtWidgets import QMenu, QDialog
+from PyQt5.QtWidgets import QMenu
 
 from E5Gui import E5MessageBox
 from E5Gui.E5Action import E5Action
@@ -25,10 +24,10 @@
 import Utilities
 
 from .FlaskCommandDialog import FlaskCommandDialog
-from .PyBabelCommandDialog import PyBabelCommandDialog
+
+from .FlaskBabelExtension.PyBabelProjectExtension import PyBabelProject
 
 
-# TODO: move PyBabel related code to a separate package (FlaskBabelExtension)
 # TODO: move database related code to a separate package (FlaskMigrateExtension)
 class Project(QObject):
     """
@@ -72,10 +71,9 @@
             "werkzeug": "",
         }
         
-        self.__capabilities = {
-            "pybabel": False,
-            "migrate": False,
-        }
+        self.__capabilities = {}
+        
+        self.__pybabelProject = PyBabelProject(self.__plugin, self, self.__ui)
     
     def initActions(self):
         """
@@ -201,7 +199,8 @@
             """<b>Configure PyBabel</b>"""
             """<p>Shows a dialog to edit the configuration for pybabel.</p>"""
         ))
-        self.pybabelConfigAct.triggered.connect(self.__configurePybabel)
+        self.pybabelConfigAct.triggered.connect(
+            self.__pybabelProject.configurePyBabel)
         self.actions.append(self.pybabelConfigAct)
         
         ##################################
@@ -296,21 +295,6 @@
         """
         return list(self.__menus.keys())
     
-    def registerOpenHook(self):
-        """
-        Public method to register the open hook to open a translations file
-        in a translations editor.
-        """
-        if self.__hooksInstalled:
-            editor = self.__plugin.getPreferences("TranslationsEditor")
-            if editor:
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "open", self.openPOEditor,
-                    self.tr("Open with {0}").format(
-                        os.path.basename(editor)))
-            else:
-                self.__translationsBrowser.removeHookMethod("open")
-    
     def projectOpenedHooks(self):
         """
         Public method to add our hook methods.
@@ -325,66 +309,19 @@
 ##            
             self.__determineCapabilities()
             
-            if self.__capabilities["pybabel"]:
-                self.__e5project.projectLanguageAddedByCode.connect(
-                    self.__projectLanguageAdded)
-                self.__translationsBrowser = (
-                    e5App().getObject("ProjectBrowser")
-                    .getProjectBrowser("translations"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "extractMessages", self.extractMessages,
-                    self.tr("Extract Messages"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "releaseAll", self.compileCatalogs,
-                    self.tr("Compile All Catalogs"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "releaseSelected", self.compileSelectedCatalogs,
-                    self.tr("Compile Selected Catalogs"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "generateAll", self.updateCatalogs,
-                    self.tr("Update All Catalogs"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "generateAllWithObsolete", self.updateCatalogsObsolete,
-                    self.tr("Update All Catalogs (with obsolete)"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "generateSelected", self.updateSelectedCatalogs,
-                    self.tr("Update Selected Catalogs"))
-                self.__translationsBrowser.addHookMethodAndMenuEntry(
-                    "generateSelectedWithObsolete",
-                    self.updateSelectedCatalogsObsolete,
-                    self.tr("Update Selected Catalogs (with obsolete)"))
-                
-                self.__hooksInstalled = True
-            
-                self.registerOpenHook()
+            self.__pybabelProject.projectOpenedHooks()
+##            self.__hooksInstalled = True
     
     def projectClosedHooks(self):
         """
         Public method to remove our hook methods.
         """
+        self.__pybabelProject.projectClosedHooks()
+        
         if self.__hooksInstalled:
 ##            self.__formsBrowser.removeHookMethod("newForm")
 ##            self.__formsBrowser = None
-##            
-            self.__e5project.projectLanguageAddedByCode.disconnect(
-                self.__projectLanguageAdded)
-            self.__translationsBrowser.removeHookMethod(
-                "extractMessages")
-            self.__translationsBrowser.removeHookMethod(
-                "releaseAll")
-            self.__translationsBrowser.removeHookMethod(
-                "releaseSelected")
-            self.__translationsBrowser.removeHookMethod(
-                "generateAll")
-            self.__translationsBrowser.removeHookMethod(
-                "generateAllWithObsolete")
-            self.__translationsBrowser.removeHookMethod(
-                "generateSelected")
-            self.__translationsBrowser.removeHookMethod(
-                "generateSelectedWithObsolete")
-            self.__translationsBrowser.removeHookMethod(
-                "open")
-            self.__translationsBrowser = None
+            pass
         
         self.__hooksInstalled = False
     
@@ -483,20 +420,11 @@
         @return full flask command
         @rtype str
         """
-        return self.__getFullCommand("flask")
+        return self.getFullCommand("flask")
     
-    def getBabelCommand(self):
+    def getFullCommand(self, command):
         """
-        Public method to build the Babel command.
-        
-        @return full pybabel command
-        @rtype str
-        """
-        return self.__getFullCommand("pybabel")
-    
-    def __getFullCommand(self, command):
-        """
-        Private method to get the full command for a given command name.
+        Public method to get the full command for a given command name.
         
         @param command command name
         @type str
@@ -683,12 +611,37 @@
         extensions.
         """
         # 1. support for flask-babel (i.e. pybabel)
-        self.__capabilities["pybabel"] = self.flaskBabelAvailable()
-        self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"])
+        self.__pybabelProject.determineCapability()
+        self.pybabelConfigAct.setEnabled(self.hasCapability("pybabel"))
         
         # 2. support for flask-migrate
         # TODO: add support for flask-migrate
     
+    def hasCapability(self, key):
+        """
+        Public method to check, if a capability is available.
+        
+        @param key key of the capability to check
+        @type str
+        @return flag indicating the availability of the capability
+        @rtype bool
+        """
+        try:
+            return self.__capabilities[key]
+        except KeyError:
+            return False
+    
+    def setCapability(self, key, available):
+        """
+        Public method to set the availability status of a capability.
+        
+        @param key key of the capability to set
+        @type str
+        @param available flag indicating the availability of the capability
+        @type bool
+        """
+        self.__capabilities[key] = available
+    
     ##################################################################
     ## slot below implements project specific flask configuration
     ##################################################################
@@ -821,431 +774,3 @@
         dlg = FlaskCommandDialog(self)
         if dlg.startCommand("init-db"):
             dlg.exec()
-    
-    ##################################################################
-    ## slots and methods below implement i18n and l10n support
-    ##################################################################
-    
-    def flaskBabelAvailable(self):
-        """
-        Public method to check, if the 'flask-babel' package is available.
-        
-        @return flag indicating the availability of 'flask-babel'
-        @rtype bool
-        """
-        venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3")
-        interpreter = self.__virtualEnvManager.getVirtualenvInterpreter(
-            venvName)
-        if interpreter and Utilities.isinpath(interpreter):
-            detector = os.path.join(
-                os.path.dirname(__file__), "FlaskBabelDetector.py")
-            proc = QProcess()
-            proc.setProcessChannelMode(QProcess.MergedChannels)
-            proc.start(interpreter, [detector])
-            finished = proc.waitForFinished(30000)
-            if finished and proc.exitCode() == 0:
-                return True
-        
-        return False
-    
-    @pyqtSlot()
-    def __configurePybabel(self):
-        """
-        Private slot to show a dialog to edit the pybabel configuration.
-        """
-        from .PyBabelConfigDialog import PyBabelConfigDialog
-        
-        config = self.getData("pybabel", "")
-        dlg = PyBabelConfigDialog(config)
-        if dlg.exec() == QDialog.Accepted:
-            config = dlg.getConfiguration()
-            self.setData("pybabel", "", config)
-            
-            self.__e5project.setTranslationPattern(os.path.join(
-                config["translationsDirectory"], "%language%", "LC_MESSAGES",
-                "{0}.po".format(config["domain"])
-            ))
-            self.__e5project.setDirty(True)
-            
-            cfgFileName = self.__e5project.getAbsoluteUniversalPath(
-                config["configFile"])
-            if not os.path.exists(cfgFileName):
-                self.__createBabelCfg(cfgFileName)
-    
-    def __ensurePybabelConfigured(self):
-        """
-        Private method to ensure, that PyBabel has been configured.
-        
-        @return flag indicating successful configuration
-        @rtype bool
-        """
-        config = self.getData("pybabel", "")
-        if not config:
-            self.__configurePybabel()
-            return True
-        
-        configFileName = self.getData("pybabel", "configFile")
-        if configFileName:
-            cfgFileName = self.__e5project.getAbsoluteUniversalPath(
-                configFileName)
-            if os.path.exists(cfgFileName):
-                return True
-            else:
-                return self.__createBabelCfg(cfgFileName)
-        
-        return False
-    
-    def __createBabelCfg(self, configFile):
-        """
-        Private method to create a template pybabel configuration file.
-        
-        @param configFile name of the configuration file to be created
-        @type str
-        @return flag indicating successful configuration file creation
-        @rtype bool
-        """
-        _, app = self.getApplication()
-        if app.endswith(".py"):
-            template = (
-                "[python: {0}]\n"
-                "[jinja2: templates/**.html]\n"
-                "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
-            )
-        else:
-            template = (
-                "[python: {0}/**.py]\n"
-                "[jinja2: {0}/templates/**.html]\n"
-                "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
-            )
-        try:
-            with open(configFile, "w") as f:
-                f.write(template.format(app))
-            self.__e5project.appendFile(configFile)
-            E5MessageBox.information(
-                None,
-                self.tr("Generate PyBabel Configuration File"),
-                self.tr("""The PyBabel configuration file was created."""
-                        """ Please edit it to adjust the entries as"""
-                        """ required.""")
-            )
-            return True
-        except EnvironmentError as err:
-            E5MessageBox.warning(
-                None,
-                self.tr("Generate PyBabel Configuration File"),
-                self.tr("""<p>The PyBabel Configuration File could not be"""
-                        """ generated.</p><p>Reason: {0}</p>""")
-                .format(str(err))
-            )
-            return False
-    
-    def __getLocale(self, filename):
-        """
-        Private method to extract the locale out of a file name.
-        
-        @param filename name of the file used for extraction
-        @type str
-        @return extracted locale
-        @rtype str or None
-        """
-        if self.__e5project.getTranslationPattern():
-            filename = os.path.splitext(filename)[0] + ".po"
-            
-            # On Windows, path typically contains backslashes. This leads
-            # to an invalid search pattern '...\(' because the opening bracket
-            # will be escaped.
-            pattern = self.__e5project.getTranslationPattern()
-            pattern = os.path.normpath(pattern)
-            pattern = pattern.replace("%language%", "(.*?)")
-            pattern = pattern.replace('\\', '\\\\')
-            match = re.search(pattern, filename)
-            if match is not None:
-                return match.group(1)
-        
-        return None
-    
-    def openPOEditor(self, poFile):
-        """
-        Public method to edit the given file in an external .po editor.
-        
-        @param poFile name of the .po file
-        @type str
-        """
-        editor = self.__plugin.getPreferences("TranslationsEditor")
-        if poFile.endswith(".po") and editor:
-            wd, _ = self.getApplication()
-            started, pid = QProcess.startDetached(editor, [poFile], wd)
-            if not started:
-                E5MessageBox.critical(
-                    None,
-                    self.tr('Process Generation Error'),
-                    self.tr('The translations editor process ({0}) could'
-                            ' not be started.').format(
-                        os.path.basename(editor)))
-    
-    def extractMessages(self):
-        """
-        Public method to extract the messages catalog template file.
-        """
-        title = self.tr("Extract messages")
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            potFile = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "catalogFile"))
-            
-            try:
-                potFilePath = os.path.dirname(potFile)
-                os.makedirs(potFilePath)
-            except EnvironmentError:
-                pass
-            
-            args = [
-                "-F",
-                os.path.relpath(
-                    self.__e5project.getAbsoluteUniversalPath(
-                        self.getData("pybabel", "configFile")),
-                    workdir
-                )
-            ]
-            if self.getData("pybabel", "markersList"):
-                for marker in self.getData("pybabel", "markersList"):
-                    args += ["-k", marker]
-            args += [
-                "-o",
-                os.path.relpath(potFile, workdir),
-                "."
-            ]
-            
-            dlg = PyBabelCommandDialog(
-                self, title,
-                msgSuccess=self.tr("\nMessages extracted successfully.")
-            )
-            res = dlg.startCommand("extract", args, workdir)
-            if res:
-                dlg.exec()
-                self.__e5project.appendFile(potFile)
-    
-    def __projectLanguageAdded(self, code):
-        """
-        Private slot handling the addition of a new language.
-        
-        @param code language code of the new language
-        @type str
-        """
-        title = self.tr(
-            "Initializing message catalog for '{0}'").format(code)
-        
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            langFile = self.__e5project.getAbsoluteUniversalPath(
-                self.__e5project.getTranslationPattern().replace(
-                    "%language%", code))
-            potFile = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "catalogFile"))
-            
-            args = [
-                "--domain={0}".format(self.getData("pybabel", "domain")),
-                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
-                "--output-file={0}".format(os.path.relpath(langFile, workdir)),
-                "--locale={0}".format(code),
-            ]
-            
-            dlg = PyBabelCommandDialog(
-                self, title,
-                msgSuccess=self.tr(
-                    "\nMessage catalog initialized successfully.")
-            )
-            res = dlg.startCommand("init", args, workdir)
-            if res:
-                dlg.exec()
-                
-                self.__e5project.appendFile(langFile)
-    
-    def compileCatalogs(self, filenames):
-        """
-        Public method to compile the message catalogs.
-        
-        @param filenames list of filenames (not used)
-        @type list of str
-        """
-        title = self.tr("Compiling message catalogs")
-        
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "translationsDirectory"))
-            
-            args = [
-                "--domain={0}".format(self.getData("pybabel", "domain")),
-                "--directory={0}".format(
-                    os.path.relpath(translationsDirectory, workdir)),
-                "--use-fuzzy",
-                "--statistics",
-            ]
-            
-            dlg = PyBabelCommandDialog(
-                self, title,
-                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
-            )
-            res = dlg.startCommand("compile", args, workdir)
-            if res:
-                dlg.exec()
-            
-                for entry in os.walk(translationsDirectory):
-                    for fileName in entry[2]:
-                        fullName = os.path.join(entry[0], fileName)
-                        if fullName.endswith('.mo'):
-                            self.__e5project.appendFile(fullName)
-    
-    def compileSelectedCatalogs(self, filenames):
-        """
-        Public method to update the message catalogs.
-        
-        @param filenames list of file names
-        @type list of str
-        """
-        title = self.tr("Compiling message catalogs")
-        
-        locales = {self.__getLocale(f) for f in filenames}
-        
-        if len(locales) == 0:
-            E5MessageBox.warning(
-                self.__ui,
-                title,
-                self.tr('No locales detected. Aborting...'))
-            return
-        
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "translationsDirectory"))
-            
-            argsList = []
-            for loc in locales:
-                argsList.append([
-                    "compile",
-                    "--domain={0}".format(self.getData("pybabel", "domain")),
-                    "--directory={0}".format(
-                        os.path.relpath(translationsDirectory, workdir)),
-                    "--use-fuzzy",
-                    "--statistics",
-                    "--locale={0}".format(loc),
-                ])
-            
-            dlg = PyBabelCommandDialog(
-                self, title=title,
-                msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
-            )
-            res = dlg.startBatchCommand(argsList, workdir)
-            if res:
-                dlg.exec()
-            
-                for entry in os.walk(translationsDirectory):
-                    for fileName in entry[2]:
-                        fullName = os.path.join(entry[0], fileName)
-                        if fullName.endswith('.mo'):
-                            self.__e5project.appendFile(fullName)
-    
-    def updateCatalogs(self, filenames, withObsolete=False):
-        """
-        Public method to update the message catalogs.
-        
-        @param filenames list of filenames (not used)
-        @type list of str
-        @param withObsolete flag indicating to keep obsolete translations
-        @type bool
-        """
-        title = self.tr("Updating message catalogs")
-        
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "translationsDirectory"))
-            potFile = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "catalogFile"))
-            
-            args = [
-                "--domain={0}".format(self.getData("pybabel", "domain")),
-                "--input-file={0}".format(os.path.relpath(potFile, workdir)),
-                "--output-dir={0}".format(
-                    os.path.relpath(translationsDirectory, workdir)),
-            ]
-            if not withObsolete:
-                args.append("--ignore-obsolete")
-            
-            dlg = PyBabelCommandDialog(
-                self, title,
-                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
-            )
-            res = dlg.startCommand("update", args, workdir)
-            if res:
-                dlg.exec()
-    
-    def updateCatalogsObsolete(self, filenames):
-        """
-        Public method to update the message catalogs keeping obsolete
-        translations.
-        
-        @param filenames list of filenames (not used)
-        @type list of str
-        """
-        self.updateCatalogs(filenames, withObsolete=True)
-    
-    def updateSelectedCatalogs(self, filenames, withObsolete=False):
-        """
-        Public method to update the selected message catalogs.
-        
-        @param filenames list of filenames
-        @type list of str
-        @param withObsolete flag indicating to keep obsolete translations
-        @type bool
-        """
-        title = self.tr("Updating message catalogs")
-        
-        locales = {self.__getLocale(f) for f in filenames}
-        
-        if len(locales) == 0:
-            E5MessageBox.warning(
-                self.__ui,
-                title,
-                self.tr('No locales detected. Aborting...'))
-            return
-        
-        if self.__ensurePybabelConfigured():
-            workdir = self.getApplication()[0]
-            translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "translationsDirectory"))
-            potFile = self.__e5project.getAbsoluteUniversalPath(
-                self.getData("pybabel", "catalogFile"))
-            argsList = []
-            for loc in locales:
-                args = [
-                    "update",
-                    "--domain={0}".format(self.getData("pybabel", "domain")),
-                    "--input-file={0}".format(
-                        os.path.relpath(potFile, workdir)),
-                    "--output-dir={0}".format(
-                        os.path.relpath(translationsDirectory, workdir)),
-                    "--locale={0}".format(loc),
-                ]
-                if not withObsolete:
-                    args.append("--ignore-obsolete")
-                argsList.append(args)
-            
-            dlg = PyBabelCommandDialog(
-                self, title=title,
-                msgSuccess=self.tr("\nMessage catalogs updated successfully.")
-            )
-            res = dlg.startBatchCommand(argsList, workdir)
-            if res:
-                dlg.exec()
-    
-    def updateSelectedCatalogsObsolete(self, filenames):
-        """
-        Public method to update the message catalogs keeping obsolete
-        translations.
-        
-        @param filenames list of filenames (not used)
-        @type list of str
-        """
-        self.updateSelectedCatalogs(filenames, withObsolete=True)
--- a/ProjectFlask/PyBabelCommandDialog.py	Sat Nov 21 17:50:57 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing a dialog to run a flask command and show its output.
-"""
-
-from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton
-
-from E5Gui import E5MessageBox
-
-from .Ui_FlaskCommandDialog import Ui_FlaskCommandDialog
-
-
-class PyBabelCommandDialog(QDialog, Ui_FlaskCommandDialog):
-    """
-    Class implementing a dialog to run a flask command and show its output.
-    """
-    def __init__(self, project, title="", msgSuccess="", msgError="",
-                 parent=None):
-        """
-        Constructor
-        
-        @param project reference to the project object
-        @type Project
-        @param title window title of the dialog
-        @type str
-        @param msgSuccess success message to be shown
-        @type str
-        @param msgError message to be shown on error
-        @type str
-        @param parent reference to the parent widget
-        @type QWidget
-        """
-        super(PyBabelCommandDialog, self).__init__(parent)
-        self.setupUi(self)
-        
-        if title:
-            self.setWindowTitle(title)
-        
-        self.__project = project
-        self.__successMessage = msgSuccess
-        self.__errorMessage = msgError
-        
-        self.__process = None
-        self.__argsLists = []
-        self.__workdir = ""
-        
-        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
-        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
-        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
-    
-    def startCommand(self, command, args, workdir, clearOutput=True):
-        """
-        Public method to start a pybabel command and show its output.
-        
-        @param command pybabel command to be run
-        @type str
-        @param args list of command line arguments for the command
-        @type list of str
-        @param workdir working directory for the command
-        @type str
-        @param clearOutput flag indicating to clear the output
-        @type bool
-        @return flag indicating a successful start
-        @rtype bool
-        """
-        babelCommand = self.__project.getBabelCommand()
-        
-        self.__process = QProcess()
-        self.__process.setWorkingDirectory(workdir)
-        self.__process.setProcessChannelMode(QProcess.MergedChannels)
-        
-        self.__process.readyReadStandardOutput.connect(self.__readStdOut)
-        self.__process.finished.connect(self.__processFinished)
-        
-        if clearOutput:
-            self.outputEdit.clear()
-        
-        babelArgs = [command]
-        if args:
-            babelArgs += args
-        
-        self.__process.start(babelCommand, babelArgs)
-        ok = self.__process.waitForStarted(10000)
-        if not ok:
-            E5MessageBox.critical(
-                None,
-                self.tr("Execute PyBabel Command"),
-                self.tr("""The pybabel process could not be started."""))
-        else:
-            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
-            self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
-            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
-            self.buttonBox.button(QDialogButtonBox.Cancel).setFocus(
-                Qt.OtherFocusReason)
-        
-        return ok
-    
-    def startBatchCommand(self, argsLists, workdir):
-        """
-        Public method to start a pybabel command repeatedly with a list of
-        arguments and show the output.
-        
-        @param argsLists list of command line arguments for the batch commands
-        @type list of lists of str
-        @param workdir working directory for the command
-        @type str
-        @return flag indicating a successful start of the first process
-        @rtype bool
-        """
-        self.__argsLists = argsLists[:]
-        self.__workdir = workdir
-        
-        # start the first process
-        args = self.__argsLists.pop(0)
-        res = self.startCommand(args[0], args[1:], workdir)
-        if not res:
-            self.__argsLists = []
-        
-        return res
-    
-    def closeEvent(self, evt):
-        """
-        Protected method handling the close event of the dialog.
-        
-        @param evt reference to the close event object
-        @type QCloseEvent
-        """
-        self.__argsLists = []
-        self.__cancelProcess()
-        evt.accept()
-    
-    @pyqtSlot()
-    def __readStdOut(self):
-        """
-        Private slot to add the server process output to the output pane.
-        """
-        if self.__process is not None:
-            out = str(self.__process.readAllStandardOutput(), "utf-8")
-            self.outputEdit.insertPlainText(out)
-    
-    def __processFinished(self, exitCode, exitStatus):
-        """
-        Private slot connected to the finished signal.
-        
-        @param exitCode exit code of the process
-        @type int
-        @param exitStatus exit status of the process
-        @type QProcess.ExitStatus
-        """
-        normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0)
-        self.__cancelProcess()
-        
-        if self.__argsLists:
-            args = self.__argsLists.pop(0)
-            self.startCommand(args[0], args[1:], self.__workdir,
-                              clearOutput=False)
-            return
-        
-        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
-        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
-        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
-        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
-            Qt.OtherFocusReason)
-        
-        if normal and self.__successMessage:
-            self.outputEdit.insertPlainText(self.__successMessage)
-        elif not normal and self.__errorMessage:
-            self.outputEdit.insertPlainText(self.__errorMessage)
-    
-    @pyqtSlot()
-    def __cancelProcess(self):
-        """
-        Private slot to terminate the current process.
-        """
-        if (
-            self.__process is not None and
-            self.__process.state() != QProcess.NotRunning
-        ):
-            self.__process.terminate()
-            QTimer.singleShot(2000, self.__process.kill)
-            self.__process.waitForFinished(3000)
-        
-        self.__process = None
-    
-    @pyqtSlot(QAbstractButton)
-    def on_buttonBox_clicked(self, button):
-        """
-        Private slot handling presses of the button box buttons.
-        
-        @param button reference to the button been clicked
-        @type QAbstractButton
-        """
-        if button is self.buttonBox.button(QDialogButtonBox.Close):
-            self.close()
-        elif button is self.buttonBox.button(QDialogButtonBox.Cancel):
-            self.__argsLists = []
-            self.__cancelProcess()
--- a/ProjectFlask/PyBabelConfigDialog.py	Sat Nov 21 17:50:57 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing a dialog to edit the PyBabel configuration.
-"""
-
-import os
-
-from PyQt5.QtCore import pyqtSlot, Qt
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox
-
-from E5Gui.E5PathPicker import E5PathPickerModes
-from E5Gui.E5Application import e5App
-
-from .Ui_PyBabelConfigDialog import Ui_PyBabelConfigDialog
-
-
-class PyBabelConfigDialog(QDialog, Ui_PyBabelConfigDialog):
-    """
-    Class implementing a dialog to edit the PyBabel configuration.
-    """
-    def __init__(self, configuration, parent=None):
-        """
-        Constructor
-        
-        @param configuration current pybabel configuration
-        @type dict
-        @param parent reference to the parent widget
-        @type QWidget
-        """
-        super(PyBabelConfigDialog, self).__init__(parent)
-        self.setupUi(self)
-        
-        self.__e5project = e5App().getObject("Project")
-        
-        self.configFilePicker.setMode(
-            E5PathPickerModes.SaveFileEnsureExtensionMode)
-        self.configFilePicker.setFilters(self.tr(
-            "Configuration Files (*.cfg);;"
-            "All Files (*)"
-        ))
-        self.configFilePicker.setDefaultDirectory(
-            self.__e5project.getProjectPath())
-        
-        self.translationsDirectoryPicker.setMode(
-            E5PathPickerModes.DirectoryMode)
-        self.translationsDirectoryPicker.setDefaultDirectory(
-            self.__e5project.getProjectPath())
-        
-        self.catalogFilePicker.setMode(
-            E5PathPickerModes.SaveFileEnsureExtensionMode)
-        self.catalogFilePicker.setFilters(self.tr(
-            "Message Catalog Files (*.pot);;"
-            "All Files (*)"
-        ))
-        self.catalogFilePicker.setDefaultDirectory(
-            self.__e5project.getProjectPath())
-        
-        self.configFilePicker.setFocus(Qt.OtherFocusReason)
-        
-        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
-        
-        if "configFile" in configuration:
-            self.configFilePicker.setText(
-                self.__e5project.getAbsoluteUniversalPath(
-                    configuration["configFile"]))
-        if "translationsDirectory" in configuration:
-            self.translationsDirectoryPicker.setText(
-                self.__e5project.getAbsoluteUniversalPath(
-                    configuration["translationsDirectory"]))
-        if "domain" in configuration:
-            self.domainEdit.setText(configuration["domain"])
-        if "catalogFile" in configuration:
-            self.catalogFilePicker.setText(
-                self.__e5project.getAbsoluteUniversalPath(
-                    configuration["catalogFile"]))
-        if "markersList" in configuration:
-            self.markersEdit.setText(" ".join(configuration["markersList"]))
-        
-        msh = self.minimumSizeHint()
-        self.resize(max(self.width(), msh.width()), msh.height())
-    
-    def getConfiguration(self):
-        """
-        Public method to get the entered configuration data.
-        
-        @return pybabel configuration
-        @rtype dict
-        """
-        configuration = {
-            "configFile": self.__e5project.getRelativeUniversalPath(
-                self.configFilePicker.text()),
-            "translationsDirectory": self.__e5project.getRelativeUniversalPath(
-                self.translationsDirectoryPicker.text()),
-        }
-        
-        domain = self.domainEdit.text()
-        if domain:
-            configuration["domain"] = domain
-        else:
-            configuration["domain"] = "messages"
-        
-        catalogFile = self.catalogFilePicker.text()
-        if not catalogFile:
-            # use a default name made of translations dir and domain
-            catalogFile = os.path.join(
-                configuration["translationsDirectory"],
-                "{0}.pot".format(configuration["domain"]))
-        configuration["catalogFile"] = (
-            self.__e5project.getRelativeUniversalPath(catalogFile)
-        )
-        
-        if self.markersEdit.text():
-            configuration["markersList"] = self.markersEdit.text().split()
-        
-        return configuration
-    
-    def __updateOK(self):
-        """
-        Private method to update the status of the OK button.
-        """
-        enable = (
-            bool(self.configFilePicker.text()) and
-            bool(self.translationsDirectoryPicker.text())
-        )
-        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable)
-    
-    def __updateCatalogPicker(self):
-        """
-        Private method to update the contents of the catalog picker.
-        """
-        translationsDirectory = self.translationsDirectoryPicker.text()
-        domain = self.domainEdit.text()
-        self.catalogFilePicker.setText(os.path.join(
-            translationsDirectory, "{0}.pot".format(domain)))
-    
-    @pyqtSlot(str)
-    def on_configFilePicker_textChanged(self, txt):
-        """
-        Private slot to handle a change of the configuration file name.
-        
-        @param txt configuration file name
-        @type str
-        """
-        self.__updateOK()
-    
-    @pyqtSlot(str)
-    def on_translationsDirectoryPicker_textChanged(self, txt):
-        """
-        Private slot to handle a change of the catalog file name.
-        
-        @param txt configuration file name
-        @type str
-        """
-        self.__updateOK()
-        self.__updateCatalogPicker()
-    
-    @pyqtSlot(str)
-    def on_domainEdit_textChanged(self, txt):
-        """
-        Private slot to handle a change of the translations domain.
-        
-        @param txt entered translations domain
-        @type str
-        """
-        self.__updateCatalogPicker()
--- a/ProjectFlask/PyBabelConfigDialog.ui	Sat Nov 21 17:50:57 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>PyBabelConfigDialog</class>
- <widget class="QDialog" name="PyBabelConfigDialog">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>600</width>
-    <height>150</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>PyBabel Configuration</string>
-  </property>
-  <property name="sizeGripEnabled">
-   <bool>true</bool>
-  </property>
-  <layout class="QGridLayout" name="gridLayout">
-   <item row="0" column="0">
-    <widget class="QLabel" name="label">
-     <property name="text">
-      <string>Configuration File:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="0" column="1">
-    <widget class="E5PathPicker" name="configFilePicker" native="true">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
-     </property>
-     <property name="focusPolicy">
-      <enum>Qt::StrongFocus</enum>
-     </property>
-     <property name="toolTip">
-      <string>Enter the name of the PyBabel configuration file</string>
-     </property>
-    </widget>
-   </item>
-   <item row="1" column="0">
-    <widget class="QLabel" name="label_4">
-     <property name="text">
-      <string>Translations Directory:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="1" column="1">
-    <widget class="E5PathPicker" name="translationsDirectoryPicker" native="true">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
-     </property>
-     <property name="focusPolicy">
-      <enum>Qt::StrongFocus</enum>
-     </property>
-     <property name="toolTip">
-      <string>Enter the name of the directory containing the translations</string>
-     </property>
-    </widget>
-   </item>
-   <item row="2" column="0">
-    <widget class="QLabel" name="label_5">
-     <property name="text">
-      <string>Domain:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="2" column="1">
-    <widget class="QLineEdit" name="domainEdit">
-     <property name="toolTip">
-      <string>Enter the name of the translations domain (leave empty for default)</string>
-     </property>
-     <property name="clearButtonEnabled">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
-   <item row="3" column="0">
-    <widget class="QLabel" name="label_2">
-     <property name="text">
-      <string>Message Catalog:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="3" column="1">
-    <widget class="E5PathPicker" name="catalogFilePicker" native="true">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
-     </property>
-     <property name="focusPolicy">
-      <enum>Qt::StrongFocus</enum>
-     </property>
-     <property name="toolTip">
-      <string>Enter the name of the message catalog file</string>
-     </property>
-    </widget>
-   </item>
-   <item row="4" column="0">
-    <widget class="QLabel" name="label_3">
-     <property name="text">
-      <string>Translation Markers:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="4" column="1">
-    <widget class="QLineEdit" name="markersEdit">
-     <property name="toolTip">
-      <string>Enter the translation markers separated by space (_ is included by default)</string>
-     </property>
-     <property name="clearButtonEnabled">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
-   <item row="5" column="0" colspan="2">
-    <widget class="QDialogButtonBox" name="buttonBox">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="standardButtons">
-      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
-     </property>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>E5PathPicker</class>
-   <extends>QWidget</extends>
-   <header>E5Gui/E5PathPicker.h</header>
-   <container>1</container>
-  </customwidget>
- </customwidgets>
- <tabstops>
-  <tabstop>configFilePicker</tabstop>
-  <tabstop>translationsDirectoryPicker</tabstop>
-  <tabstop>domainEdit</tabstop>
-  <tabstop>catalogFilePicker</tabstop>
-  <tabstop>markersEdit</tabstop>
- </tabstops>
- <resources/>
- <connections>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>accepted()</signal>
-   <receiver>PyBabelConfigDialog</receiver>
-   <slot>accept()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>248</x>
-     <y>254</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>157</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>rejected()</signal>
-   <receiver>PyBabelConfigDialog</receiver>
-   <slot>reject()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>316</x>
-     <y>260</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>286</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>

eric ide

mercurial