Mon, 28 Oct 2024 17:11:28 +0100
- changed to the new style header
- ensured proper parent relationship of modal dialogs
- included compiled form files
# -*- coding: utf-8 -*- # Copyright (c) 2020 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the project support for flask-babel. """ import contextlib import os import re from PyQt6.QtCore import QObject, QProcess, pyqtSlot from PyQt6.QtWidgets import QDialog, QMenu from eric7.EricGui.EricAction import EricAction from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp try: from eric7.SystemUtilities.FileSystemUtilities import isinpath except ImportError: # imports for eric < 23.1 from eric7.Utilities import isinpath from .PyBabelCommandDialog import PyBabelCommandDialog class PyBabelProject(QObject): """ Class implementing the flask-babel 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 reference to the parent widget @type QWidget """ super().__init__(parent) self.__plugin = plugin self.__project = project self.__ui = parent self.__ericProject = ericApp().getObject("Project") self.__hooksInstalled = False def initActions(self): """ Public method to define the flask-babel actions. """ self.actions = [] self.pybabelConfigAct = EricAction( self.tr("Configure flask-babel"), self.tr("&Configure flask-babel"), 0, 0, self, "flask_config_pybabel", ) self.pybabelConfigAct.setStatusTip( self.tr("Shows a dialog to edit the configuration for flask-babel") ) self.pybabelConfigAct.setWhatsThis( self.tr( """<b>Configure flask-babel</b>""" """<p>Shows a dialog to edit the configuration for """ """flask-babel.</p>""" ) ) self.pybabelConfigAct.triggered.connect(self.__configurePyBabel) self.actions.append(self.pybabelConfigAct) self.pybabelInstallAct = EricAction( self.tr("Install flask-babel"), self.tr("&Install flask-babel"), 0, 0, self, "flask_install_pybabel", ) self.pybabelInstallAct.setStatusTip( self.tr( "Installs the flask-babel extension into the configured environment" ) ) self.pybabelInstallAct.setWhatsThis( self.tr( """<b>Install flask-babel</b>""" """<p>Installs the flask-babel extension into the configured""" """ environment using the pip interface.</p>""" ) ) self.pybabelInstallAct.triggered.connect(self.__installFlaskBabel) self.actions.append(self.pybabelInstallAct) self.pybabelAvailabilityAct = EricAction( self.tr("Check flask-babel Availability"), self.tr("Check flask-babel &Availability"), 0, 0, self, "flask_check_pybabel", ) self.pybabelAvailabilityAct.setStatusTip( self.tr("Check the availability of the flask-babel extension") ) self.pybabelAvailabilityAct.setWhatsThis( self.tr( """<b>Check flask-babel Availability</b>""" """<p>Check the availability of the flask-babel extension.</p>""" ) ) self.pybabelAvailabilityAct.triggered.connect(self.__checkAvailability) self.actions.append(self.pybabelAvailabilityAct) def initMenu(self): """ Public method to initialize the flask-babel menu. @return the menu generated @rtype QMenu """ menu = QMenu(self.tr("Translations")) menu.setTearOffEnabled(True) menu.addAction(self.pybabelConfigAct) menu.addSeparator() menu.addAction(self.pybabelAvailabilityAct) menu.addAction(self.pybabelInstallAct) return menu 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("flask-babel"): self.__ericProject.projectLanguageAddedByCode.connect( self.__projectLanguageAdded ) self.__translationsBrowser = ( ericApp().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.__ericProject.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. """ available = ( self.__project.getData("flask", "flask_babel_available") if self.__project.getData("flask", "flask_babel_override") else self.__flaskBabelAvailable() ) self.__project.setCapability("flask-babel", available) self.pybabelConfigAct.setEnabled(available) self.pybabelInstallAct.setEnabled(not available) ################################################################## ## 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): """ Private method to check, if the 'flask-babel' package is available. @return flag indicating the availability of 'flask-babel' @rtype bool """ interpreter = self.__project.getVirtualenvInterpreter() if interpreter and isinpath(interpreter): detector = os.path.join(os.path.dirname(__file__), "FlaskBabelDetector.py") proc = QProcess() proc.setProcessChannelMode(QProcess.ProcessChannelMode.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.__project.getData("flask-babel", "") dlg = PyBabelConfigDialog(config, parent=self.__ui) if dlg.exec() == QDialog.DialogCode.Accepted: config = dlg.getConfiguration() self.__project.setData("flask-babel", "", config) self.__ericProject.setTranslationPattern( os.path.join( config["translationsDirectory"], "%language%", "LC_MESSAGES", "{0}.po".format(config["domain"]), ) ) self.__ericProject.setDirty(True) cfgFileName = self.__ericProject.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("flask-babel", "") if not config: self.__configurePybabel() return True configFileName = self.__project.getData("flask-babel", "configFile") if configFileName: cfgFileName = self.__ericProject.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.__project.getApplication() template = ( ( "[python: {0}]\n" "[jinja2: templates/**.html]\n" "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n" ) if app.endswith(".py") else ( "[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.__ericProject.appendFile(configFile) EricMessageBox.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 OSError as err: EricMessageBox.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 @pyqtSlot() def __installFlaskBabel(self): """ Private slot to install the flask-babel extension into the configured environment. """ venvName = self.__project.getVirtualEnvironment() if venvName: interpreter = self.__project.getFullCommand("python") pip = ericApp().getObject("Pip") pip.installPackages(["flask-babel"], interpreter=interpreter) self.determineCapability() else: EricMessageBox.critical( None, self.tr("Install flask-babel"), self.tr( "The 'flask-babel' extension could not be installed" " because no virtual environment has been" " configured." ), ) @pyqtSlot() def __checkAvailability(self): """ Private slot to check the availability of the 'flask-babel' extension. """ self.determineCapability() msg = ( self.tr("The 'flask-babel' extension is installed.") if self.__project.hasCapability("flask-babel") else self.tr("The 'flask-babel' extension is not installed.") ) EricMessageBox.information(None, self.tr("flask-babel Availability"), msg) 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.__ericProject.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.__ericProject.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: EricMessageBox.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.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "catalogFile") ) with contextlib.suppress(OSError): potFilePath = os.path.dirname(potFile) os.makedirs(potFilePath) args = [ "-F", os.path.relpath( self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "configFile") ), workdir, ), ] if self.__project.getData("flask-babel", "markersList"): for marker in self.__project.getData("flask-babel", "markersList"): args += ["-k", marker] args += ["-o", os.path.relpath(potFile, workdir), "."] dlg = PyBabelCommandDialog( self, title, msgSuccess=self.tr("\nMessages extracted successfully."), parent=self.__ui, ) res = dlg.startCommand("extract", args, workdir) if res: dlg.exec() self.__ericProject.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.__ericProject.getAbsoluteUniversalPath( self.__ericProject.getTranslationPattern().replace("%language%", code) ) potFile = self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "catalogFile") ) args = [ "--domain={0}".format(self.__project.getData("flask-babel", "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."), parent=self.__ui, ) res = dlg.startCommand("init", args, workdir) if res: dlg.exec() self.__ericProject.appendFile(langFile) def compileCatalogs(self, filenames): # noqa: U100 """ 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.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "translationsDirectory") ) args = [ "--domain={0}".format(self.__project.getData("flask-babel", "domain")), "--directory={0}".format( os.path.relpath(translationsDirectory, workdir) ), "--use-fuzzy", "--statistics", ] dlg = PyBabelCommandDialog( self, title, msgSuccess=self.tr("\nMessage catalogs compiled successfully."), parent=self.__ui, ) 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.__ericProject.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: EricMessageBox.warning( self.__ui, title, self.tr("No locales detected. Aborting...") ) return if self.__ensurePybabelConfigured(): workdir = self.__project.getApplication()[0] translationsDirectory = self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "translationsDirectory") ) argsList = [] for loc in locales: argsList.append( [ "compile", "--domain={0}".format( self.__project.getData("flask-babel", "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."), parent=self.__ui, ) 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.__ericProject.appendFile(fullName) def updateCatalogs(self, filenames, withObsolete=False): # noqa: U100 """ 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.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "translationsDirectory") ) potFile = self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "catalogFile") ) args = [ "--domain={0}".format(self.__project.getData("flask-babel", "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."), parent=self.__ui, ) 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: EricMessageBox.warning( self.__ui, title, self.tr("No locales detected. Aborting...") ) return if self.__ensurePybabelConfigured(): workdir = self.__project.getApplication()[0] translationsDirectory = self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "translationsDirectory") ) potFile = self.__ericProject.getAbsoluteUniversalPath( self.__project.getData("flask-babel", "catalogFile") ) argsList = [] for loc in locales: args = [ "update", "--domain={0}".format( self.__project.getData("flask-babel", "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."), parent=self.__ui, ) 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)