Sat, 21 Nov 2020 17:50:57 +0100
Done implementing pybabel translations support.
# -*- coding: utf-8 -*- # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the Flask project support. """ import os import re from PyQt5.QtCore import ( pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer ) from PyQt5.QtWidgets import QMenu, QDialog from E5Gui import E5MessageBox from E5Gui.E5Action import E5Action from E5Gui.E5Application import e5App from Globals import isWindowsPlatform import UI.PixmapCache import Utilities from .FlaskCommandDialog import FlaskCommandDialog from .PyBabelCommandDialog import PyBabelCommandDialog # TODO: move PyBabel related code to a separate package (FlaskBabelExtension) # TODO: move database related code to a separate package (FlaskMigrateExtension) class Project(QObject): """ Class implementing the Flask project support. """ def __init__(self, plugin, iconSuffix, parent=None): """ Constructor @param plugin reference to the plugin object @type ProjectFlaskPlugin @param iconSuffix suffix for the icons @type str @param parent parent @type QObject """ super(Project, self).__init__(parent) self.__plugin = plugin self.__iconSuffix = iconSuffix self.__ui = parent self.__e5project = e5App().getObject("Project") self.__virtualEnvManager = e5App().getObject("VirtualEnvManager") self.__menus = {} # dictionary with references to menus self.__hooksInstalled = False self.__serverDialog = None self.__routesDialog = None self.__shellProcess = None self.__projectData = { "flask": {}, "pybabel": {}, } self.__flaskVersions = { "python": "", "flask": "", "werkzeug": "", } self.__capabilities = { "pybabel": False, "migrate": False, } def initActions(self): """ Public method to define the Flask actions. """ self.actions = [] ############################## ## run actions below ## ############################## self.runServerAct = E5Action( self.tr('Run Server'), self.tr('Run &Server'), 0, 0, self, 'flask_run_server') self.runServerAct.setStatusTip(self.tr( 'Starts the Flask Web server')) self.runServerAct.setWhatsThis(self.tr( """<b>Run Server</b>""" """<p>Starts the Flask Web server.</p>""" )) self.runServerAct.triggered.connect(self.__runServer) self.actions.append(self.runServerAct) self.runDevServerAct = E5Action( self.tr('Run Development Server'), self.tr('Run &Development Server'), 0, 0, self, 'flask_run_dev_server') self.runDevServerAct.setStatusTip(self.tr( 'Starts the Flask Web server in development mode')) self.runDevServerAct.setWhatsThis(self.tr( """<b>Run Development Server</b>""" """<p>Starts the Flask Web server in development mode.</p>""" )) self.runDevServerAct.triggered.connect(self.__runDevelopmentServer) self.actions.append(self.runDevServerAct) self.askForServerOptionsAct = E5Action( self.tr('Ask for Server Start Options'), self.tr('Ask for Server Start Options'), 0, 0, self, 'flask_ask_server_options') self.askForServerOptionsAct.setStatusTip(self.tr( 'Ask for server start options')) self.askForServerOptionsAct.setWhatsThis(self.tr( """<b>Ask for Server Start Options</b>""" """<p>Asks for server start options before the Flask Web server""" """ is started. If this is unchecked, the server is started with""" """ default parameters.</p>""" )) self.askForServerOptionsAct.setCheckable(True) self.actions.append(self.askForServerOptionsAct) ############################### ## shell action below ## ############################### self.runPythonShellAct = E5Action( self.tr('Start Flask Python Console'), self.tr('Start Flask &Python Console'), 0, 0, self, 'flask_python_console') self.runPythonShellAct.setStatusTip(self.tr( 'Starts an interactive Python interpreter')) self.runPythonShellAct.setWhatsThis(self.tr( """<b>Start Flask Python Console</b>""" """<p>Starts an interactive Python interpreter.</p>""" )) self.runPythonShellAct.triggered.connect(self.__runPythonShell) self.actions.append(self.runPythonShellAct) ################################ ## routes action below ## ################################ self.showRoutesAct = E5Action( self.tr('Show Routes'), self.tr('Show &Routes'), 0, 0, self, 'flask_show_routes') self.showRoutesAct.setStatusTip(self.tr( 'Shows a dialog with the routes of the flask app')) self.showRoutesAct.setWhatsThis(self.tr( """<b>Show Routes</b>""" """<p>Shows a dialog with the routes of the flask app.</p>""" )) self.showRoutesAct.triggered.connect(self.__showRoutes) self.actions.append(self.showRoutesAct) ################################## ## database action below ## ################################## self.initDatabaseAct = E5Action( self.tr('Initialize Database'), self.tr('&Initialize Database'), 0, 0, self, 'flask_init_database') self.initDatabaseAct.setStatusTip(self.tr( 'Shows a dialog with the result of the database creation')) self.initDatabaseAct.setWhatsThis(self.tr( """<b>Initialize Database</b>""" """<p>Shows a dialog with the result of the database""" """ creation.</p>""" )) self.initDatabaseAct.triggered.connect(self.__initDatabase) self.actions.append(self.initDatabaseAct) ################################## ## pybabel action below ## ################################## self.pybabelConfigAct = E5Action( self.tr('Configure PyBabel'), self.tr('Configure Py&Babel'), 0, 0, self, 'flask_config_pybabel') self.pybabelConfigAct.setStatusTip(self.tr( 'Shows a dialog to edit the configuration for pybabel')) self.pybabelConfigAct.setWhatsThis(self.tr( """<b>Configure PyBabel</b>""" """<p>Shows a dialog to edit the configuration for pybabel.</p>""" )) self.pybabelConfigAct.triggered.connect(self.__configurePybabel) self.actions.append(self.pybabelConfigAct) ################################## ## documentation action below ## ################################## self.documentationAct = E5Action( self.tr('Documentation'), self.tr('D&ocumentation'), 0, 0, self, 'flask_documentation') self.documentationAct.setStatusTip(self.tr( 'Shows the help viewer with the Flask documentation')) self.documentationAct.setWhatsThis(self.tr( """<b>Documentation</b>""" """<p>Shows the help viewer with the Flask documentation.</p>""" )) self.documentationAct.triggered.connect(self.__showDocumentation) self.actions.append(self.documentationAct) ############################## ## about action below ## ############################## self.aboutFlaskAct = E5Action( self.tr('About Flask'), self.tr('About &Flask'), 0, 0, self, 'flask_about') self.aboutFlaskAct.setStatusTip(self.tr( 'Shows some information about Flask')) self.aboutFlaskAct.setWhatsThis(self.tr( """<b>About Flask</b>""" """<p>Shows some information about Flask.</p>""" )) self.aboutFlaskAct.triggered.connect(self.__flaskInfo) self.actions.append(self.aboutFlaskAct) def initMenu(self): """ Public method to initialize the Flask menu. @return the menu generated @rtype QMenu """ self.__menus = {} # clear menus references menu = QMenu(self.tr('&Flask'), self.__ui) menu.setTearOffEnabled(True) menu.addAction(self.runServerAct) menu.addAction(self.runDevServerAct) menu.addAction(self.askForServerOptionsAct) menu.addSeparator() menu.addAction(self.runPythonShellAct) menu.addSeparator() menu.addAction(self.showRoutesAct) menu.addSeparator() menu.addAction(self.initDatabaseAct) menu.addSeparator() menu.addAction(self.pybabelConfigAct) menu.addSeparator() menu.addAction(self.documentationAct) menu.addSeparator() menu.addAction(self.aboutFlaskAct) self.__menus["main"] = menu return menu def getMenu(self, name): """ Public method to get a reference to the requested menu. @param name name of the menu @type str @return reference to the menu or None, if no menu with the given name exists @rtype QMenu or None """ if name in self.__menus: return self.__menus[name] else: return None def getMenuNames(self): """ Public method to get the names of all menus. @return menu names @rtype list of str """ 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. """ if self.__e5project.getProjectType() == "Flask": # TODO: add some methods for standard templates ## self.__formsBrowser = ( ## e5App().getObject("ProjectBrowser") ## .getProjectBrowser("forms")) ## self.__formsBrowser.addHookMethodAndMenuEntry( ## "newForm", self.newForm, self.tr("New template...")) ## 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() def projectClosedHooks(self): """ Public method to remove our hook methods. """ 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 self.__hooksInstalled = False ################################################################## ## slots below implement general functionality ################################################################## def projectClosed(self): """ Public method to handle the closing of a project. """ for dlg in (self.__serverDialog, self.__routesDialog): if dlg is not None: dlg.close() def supportedPythonVariants(self): """ Public method to get the supported Python variants. @return list of supported Python variants @rtype list of str """ variants = [] virtEnv = self.__getVirtualEnvironment() if virtEnv: fullCmd = self.getFlaskCommand() if fullCmd: variants.append("Python3") else: fullCmd = self.getFlaskCommand() if isWindowsPlatform(): if fullCmd: variants.append("Python3") else: fullCmds = Utilities.getExecutablePaths("flask") for fullCmd in fullCmds: try: with open(fullCmd, 'r', encoding='utf-8') as f: l0 = f.readline() except (IOError, OSError): l0 = "" if self.__isSuitableForVariant("Python3", l0): variants.append("Python3") break return variants def __isSuitableForVariant(self, variant, line0): """ Private method to test, if a detected command file is suitable for the given Python variant. @param variant Python variant to test for @type str @param line0 first line of the executable @type str @return flag indicating a suitable file was found @rtype bool """ l0 = line0.lower() ok = (variant.lower() in l0 or "{0}.".format(variant[-1]) in l0) ok |= "pypy3" in l0 return ok def __getVirtualEnvironment(self): """ Private method to get the path of the virtual environment. @return path of the virtual environment @rtype str """ language = self.__e5project.getProjectLanguage() if language == "Python3": venvName = self.__plugin.getPreferences( "VirtualEnvironmentNamePy3") else: venvName = "" if venvName: virtEnv = self.__virtualEnvManager.getVirtualenvDirectory( venvName) else: virtEnv = "" if virtEnv and not os.path.exists(virtEnv): virtEnv = "" return virtEnv # __IGNORE_WARNING_M834__ def getFlaskCommand(self): """ Public method to build the Flask command. @return full flask command @rtype str """ return self.__getFullCommand("flask") def getBabelCommand(self): """ 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. @param command command name @type str @return full command @rtype str """ virtualEnv = self.__getVirtualEnvironment() if isWindowsPlatform(): fullCmds = [ os.path.join(virtualEnv, "Scripts", command + '.exe'), os.path.join(virtualEnv, "bin", command + '.exe'), command # fall back to just cmd ] else: fullCmds = [ os.path.join(virtualEnv, "bin", command), os.path.join(virtualEnv, "local", "bin", command), Utilities.getExecutablePath(command), command # fall back to just cmd ] for command in fullCmds: if os.path.exists(command): break return command @pyqtSlot() def __flaskInfo(self): """ Private slot to show some info about Flask. """ versions = self.getFlaskVersionStrings() url = "https://palletsprojects.com/p/flask/" msgBox = E5MessageBox.E5MessageBox( E5MessageBox.Question, self.tr("About Flask"), self.tr( "<p>Flask is a lightweight WSGI web application framework." " It is designed to make getting started quick and easy," " with the ability to scale up to complex applications.</p>" "<p><table>" "<tr><td>Flask Version:</td><td>{0}</td></tr>" "<tr><td>Werkzeug Version:</td><td>{1}</td></tr>" "<tr><td>Python Version:</td><td>{2}</td></tr>" "<tr><td>Flask URL:</td><td><a href=\"{3}\">" "The Pallets Projects - Flask</a></td></tr>" "</table></p>" ).format(versions["flask"], versions["werkzeug"], versions["python"], url), modal=True, buttons=E5MessageBox.Ok) msgBox.setIconPixmap(UI.PixmapCache.getPixmap( os.path.join("ProjectFlask", "icons", "flask64-{0}".format(self.__iconSuffix)))) msgBox.exec() def getFlaskVersionStrings(self): """ Public method to get the Flask, Werkzeug and Python versions as a string. @return dictionary containing the Flask, Werkzeug and Python versions @rtype dict """ if not self.__flaskVersions["flask"]: cmd = self.getFlaskCommand() proc = QProcess() proc.start(cmd, ["--version"]) if proc.waitForFinished(10000): output = str(proc.readAllStandardOutput(), "utf-8") for line in output.lower().splitlines(): key, version = line.strip().split(None, 1) self.__flaskVersions[key] = version return self.__flaskVersions def prepareRuntimeEnvironment(self, development=False): """ Public method to prepare a QProcessEnvironment object and determine the appropriate working directory. @param development flag indicating development mode @type bool @return tuple containing the working directory and a prepared environment object to be used with QProcess @rtype tuple of (str, QProcessEnvironment) """ workdir, app = self.getApplication() env = QProcessEnvironment.systemEnvironment() env.insert("FLASK_APP", app) if development: env.insert("FLASK_ENV", "development") return workdir, env def getApplication(self): """ Public method to determine the application name and the respective working directory. @return tuple containing the working directory and the application name @rtype tuple of (str, str) """ mainScript = self.__e5project.getMainScript(normalized=True) if not mainScript: E5MessageBox.critical( self.__ui, self.tr("Prepare Environment"), self.tr("""The project has no configured main script""" """ (= Flask application). Aborting...""")) return "", None scriptPath, scriptName = os.path.split(mainScript) if scriptName == "__init__.py": workdir, app = os.path.split(scriptPath) else: workdir, app = scriptPath, scriptName return workdir, app def getData(self, category, key): """ Public method to get data stored in the project store. @param category data category @type str @param key data key @type str @return referenced data @rtype any """ if category not in self.__projectData: self.__projectData[category] = {} if not self.__projectData[category]: data = self.__e5project.getData( "PROJECTTYPESPECIFICDATA", category) if data is not None: self.__projectData[category] = data data = self.__projectData[category] if not key: # return complete category dictionary return data elif key in data: # return individual entry return data[key] else: # failure return None def setData(self, category, key, value): """ Public method to store data in the project store. @param category data category @type str @param key data key @type str @param value data to be stored @type any (serializable type) """ if category not in self.__projectData: self.__projectData[category] = {} if not self.__projectData[category]: data = self.__e5project.getData( "PROJECTTYPESPECIFICDATA", category) if data is not None: self.__projectData[category] = data if not key: # update the complete category self.__projectData[category] = value else: # update individual entry self.__projectData[category][key] = value self.__e5project.setData( "PROJECTTYPESPECIFICDATA", category, self.__projectData[category]) def __determineCapabilities(self): """ Private method to determine capabilities provided by supported extensions. """ # 1. support for flask-babel (i.e. pybabel) self.__capabilities["pybabel"] = self.flaskBabelAvailable() self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"]) # 2. support for flask-migrate # TODO: add support for flask-migrate ################################################################## ## slot below implements project specific flask configuration ################################################################## @pyqtSlot() def __configureFlaskForProject(self): """ Private slot to configure the project specific flask parameters. """ # TODO: implement the flask project config dialog # 1. check boxes to override flask-babel and flask-migrate # 2. support for project specific virtual environment ################################################################## ## slot below implements documentation function ################################################################## def __showDocumentation(self): """ Private slot to show the helpviewer with the Flask documentation. """ page = self.__plugin.getPreferences("FlaskDocUrl") self.__ui.launchHelpViewer(page) ################################################################## ## slots below implement run functions for the server ################################################################## @pyqtSlot() def __runServer(self, development=False): """ Private slot to start the Flask Web server. @param development flag indicating development mode @type bool """ from .RunServerDialog import RunServerDialog if self.__serverDialog is not None: self.__serverDialog.close() askForOptions = self.askForServerOptionsAct.isChecked() dlg = RunServerDialog(self.__plugin, self) if dlg.startServer(development=development, askForOptions=askForOptions): dlg.show() self.__serverDialog = dlg @pyqtSlot() def __runDevelopmentServer(self): """ Private slot to start the Flask Web server in development mode. """ self.__runServer(development=True) ################################################################## ## slots below implement functions for the flask console ################################################################## @pyqtSlot() def __runPythonShell(self): """ Private slot to start a Python console in the app context. """ workdir, env = self.prepareRuntimeEnvironment() if env is not None: command = self.getFlaskCommand() consoleCmd = self.__plugin.getPreferences("ConsoleCommand") if consoleCmd: self.__terminatePythonShell() args = Utilities.parseOptionString(consoleCmd) args[0] = Utilities.getExecutablePath(args[0]) args += [command, "shell"] self.__shellProcess = QProcess() self.__shellProcess.setProcessEnvironment(env) self.__shellProcess.setWorkingDirectory(workdir) self.__shellProcess.finished.connect( self.__shellProcessFinished) self.__shellProcess.start(args[0], args[1:]) self.__shellProcess.waitForStarted(10000) @pyqtSlot() def __shellProcessFinished(self): """ Private slot connected to the finished signal. """ self.__shellProcess = None def __terminatePythonShell(self): """ Private method to terminate the current Python console. """ if ( self.__shellProcess is not None and self.__shellProcess.state() != QProcess.NotRunning ): self.__shellProcess.terminate() QTimer.singleShot(2000, self.__shellProcess.kill) self.__shellProcess.waitForFinished(3000) ################################################################## ## slots below implement various debugging functions ################################################################## @pyqtSlot() def __showRoutes(self): """ Private slot showing all URL dispatch routes. """ from .RoutesDialog import RoutesDialog if self.__routesDialog is not None: self.__routesDialog.close() dlg = RoutesDialog(self) if dlg.showRoutes(): dlg.show() self.__routesDialog = dlg # TODO: replace this by commands made by flask-migrate (flask db ...) @pyqtSlot() def __initDatabase(self): """ Private slot showing the result of the database creation. """ 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)