323 ## self.__formsBrowser.addHookMethodAndMenuEntry( |
307 ## self.__formsBrowser.addHookMethodAndMenuEntry( |
324 ## "newForm", self.newForm, self.tr("New template...")) |
308 ## "newForm", self.newForm, self.tr("New template...")) |
325 ## |
309 ## |
326 self.__determineCapabilities() |
310 self.__determineCapabilities() |
327 |
311 |
328 if self.__capabilities["pybabel"]: |
312 self.__pybabelProject.projectOpenedHooks() |
329 self.__e5project.projectLanguageAddedByCode.connect( |
313 ## self.__hooksInstalled = True |
330 self.__projectLanguageAdded) |
|
331 self.__translationsBrowser = ( |
|
332 e5App().getObject("ProjectBrowser") |
|
333 .getProjectBrowser("translations")) |
|
334 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
335 "extractMessages", self.extractMessages, |
|
336 self.tr("Extract Messages")) |
|
337 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
338 "releaseAll", self.compileCatalogs, |
|
339 self.tr("Compile All Catalogs")) |
|
340 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
341 "releaseSelected", self.compileSelectedCatalogs, |
|
342 self.tr("Compile Selected Catalogs")) |
|
343 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
344 "generateAll", self.updateCatalogs, |
|
345 self.tr("Update All Catalogs")) |
|
346 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
347 "generateAllWithObsolete", self.updateCatalogsObsolete, |
|
348 self.tr("Update All Catalogs (with obsolete)")) |
|
349 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
350 "generateSelected", self.updateSelectedCatalogs, |
|
351 self.tr("Update Selected Catalogs")) |
|
352 self.__translationsBrowser.addHookMethodAndMenuEntry( |
|
353 "generateSelectedWithObsolete", |
|
354 self.updateSelectedCatalogsObsolete, |
|
355 self.tr("Update Selected Catalogs (with obsolete)")) |
|
356 |
|
357 self.__hooksInstalled = True |
|
358 |
|
359 self.registerOpenHook() |
|
360 |
314 |
361 def projectClosedHooks(self): |
315 def projectClosedHooks(self): |
362 """ |
316 """ |
363 Public method to remove our hook methods. |
317 Public method to remove our hook methods. |
364 """ |
318 """ |
|
319 self.__pybabelProject.projectClosedHooks() |
|
320 |
365 if self.__hooksInstalled: |
321 if self.__hooksInstalled: |
366 ## self.__formsBrowser.removeHookMethod("newForm") |
322 ## self.__formsBrowser.removeHookMethod("newForm") |
367 ## self.__formsBrowser = None |
323 ## self.__formsBrowser = None |
368 ## |
324 pass |
369 self.__e5project.projectLanguageAddedByCode.disconnect( |
|
370 self.__projectLanguageAdded) |
|
371 self.__translationsBrowser.removeHookMethod( |
|
372 "extractMessages") |
|
373 self.__translationsBrowser.removeHookMethod( |
|
374 "releaseAll") |
|
375 self.__translationsBrowser.removeHookMethod( |
|
376 "releaseSelected") |
|
377 self.__translationsBrowser.removeHookMethod( |
|
378 "generateAll") |
|
379 self.__translationsBrowser.removeHookMethod( |
|
380 "generateAllWithObsolete") |
|
381 self.__translationsBrowser.removeHookMethod( |
|
382 "generateSelected") |
|
383 self.__translationsBrowser.removeHookMethod( |
|
384 "generateSelectedWithObsolete") |
|
385 self.__translationsBrowser.removeHookMethod( |
|
386 "open") |
|
387 self.__translationsBrowser = None |
|
388 |
325 |
389 self.__hooksInstalled = False |
326 self.__hooksInstalled = False |
390 |
327 |
391 ################################################################## |
328 ################################################################## |
392 ## slots below implement general functionality |
329 ## slots below implement general functionality |
819 Private slot showing the result of the database creation. |
772 Private slot showing the result of the database creation. |
820 """ |
773 """ |
821 dlg = FlaskCommandDialog(self) |
774 dlg = FlaskCommandDialog(self) |
822 if dlg.startCommand("init-db"): |
775 if dlg.startCommand("init-db"): |
823 dlg.exec() |
776 dlg.exec() |
824 |
|
825 ################################################################## |
|
826 ## slots and methods below implement i18n and l10n support |
|
827 ################################################################## |
|
828 |
|
829 def flaskBabelAvailable(self): |
|
830 """ |
|
831 Public method to check, if the 'flask-babel' package is available. |
|
832 |
|
833 @return flag indicating the availability of 'flask-babel' |
|
834 @rtype bool |
|
835 """ |
|
836 venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3") |
|
837 interpreter = self.__virtualEnvManager.getVirtualenvInterpreter( |
|
838 venvName) |
|
839 if interpreter and Utilities.isinpath(interpreter): |
|
840 detector = os.path.join( |
|
841 os.path.dirname(__file__), "FlaskBabelDetector.py") |
|
842 proc = QProcess() |
|
843 proc.setProcessChannelMode(QProcess.MergedChannels) |
|
844 proc.start(interpreter, [detector]) |
|
845 finished = proc.waitForFinished(30000) |
|
846 if finished and proc.exitCode() == 0: |
|
847 return True |
|
848 |
|
849 return False |
|
850 |
|
851 @pyqtSlot() |
|
852 def __configurePybabel(self): |
|
853 """ |
|
854 Private slot to show a dialog to edit the pybabel configuration. |
|
855 """ |
|
856 from .PyBabelConfigDialog import PyBabelConfigDialog |
|
857 |
|
858 config = self.getData("pybabel", "") |
|
859 dlg = PyBabelConfigDialog(config) |
|
860 if dlg.exec() == QDialog.Accepted: |
|
861 config = dlg.getConfiguration() |
|
862 self.setData("pybabel", "", config) |
|
863 |
|
864 self.__e5project.setTranslationPattern(os.path.join( |
|
865 config["translationsDirectory"], "%language%", "LC_MESSAGES", |
|
866 "{0}.po".format(config["domain"]) |
|
867 )) |
|
868 self.__e5project.setDirty(True) |
|
869 |
|
870 cfgFileName = self.__e5project.getAbsoluteUniversalPath( |
|
871 config["configFile"]) |
|
872 if not os.path.exists(cfgFileName): |
|
873 self.__createBabelCfg(cfgFileName) |
|
874 |
|
875 def __ensurePybabelConfigured(self): |
|
876 """ |
|
877 Private method to ensure, that PyBabel has been configured. |
|
878 |
|
879 @return flag indicating successful configuration |
|
880 @rtype bool |
|
881 """ |
|
882 config = self.getData("pybabel", "") |
|
883 if not config: |
|
884 self.__configurePybabel() |
|
885 return True |
|
886 |
|
887 configFileName = self.getData("pybabel", "configFile") |
|
888 if configFileName: |
|
889 cfgFileName = self.__e5project.getAbsoluteUniversalPath( |
|
890 configFileName) |
|
891 if os.path.exists(cfgFileName): |
|
892 return True |
|
893 else: |
|
894 return self.__createBabelCfg(cfgFileName) |
|
895 |
|
896 return False |
|
897 |
|
898 def __createBabelCfg(self, configFile): |
|
899 """ |
|
900 Private method to create a template pybabel configuration file. |
|
901 |
|
902 @param configFile name of the configuration file to be created |
|
903 @type str |
|
904 @return flag indicating successful configuration file creation |
|
905 @rtype bool |
|
906 """ |
|
907 _, app = self.getApplication() |
|
908 if app.endswith(".py"): |
|
909 template = ( |
|
910 "[python: {0}]\n" |
|
911 "[jinja2: templates/**.html]\n" |
|
912 "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n" |
|
913 ) |
|
914 else: |
|
915 template = ( |
|
916 "[python: {0}/**.py]\n" |
|
917 "[jinja2: {0}/templates/**.html]\n" |
|
918 "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n" |
|
919 ) |
|
920 try: |
|
921 with open(configFile, "w") as f: |
|
922 f.write(template.format(app)) |
|
923 self.__e5project.appendFile(configFile) |
|
924 E5MessageBox.information( |
|
925 None, |
|
926 self.tr("Generate PyBabel Configuration File"), |
|
927 self.tr("""The PyBabel configuration file was created.""" |
|
928 """ Please edit it to adjust the entries as""" |
|
929 """ required.""") |
|
930 ) |
|
931 return True |
|
932 except EnvironmentError as err: |
|
933 E5MessageBox.warning( |
|
934 None, |
|
935 self.tr("Generate PyBabel Configuration File"), |
|
936 self.tr("""<p>The PyBabel Configuration File could not be""" |
|
937 """ generated.</p><p>Reason: {0}</p>""") |
|
938 .format(str(err)) |
|
939 ) |
|
940 return False |
|
941 |
|
942 def __getLocale(self, filename): |
|
943 """ |
|
944 Private method to extract the locale out of a file name. |
|
945 |
|
946 @param filename name of the file used for extraction |
|
947 @type str |
|
948 @return extracted locale |
|
949 @rtype str or None |
|
950 """ |
|
951 if self.__e5project.getTranslationPattern(): |
|
952 filename = os.path.splitext(filename)[0] + ".po" |
|
953 |
|
954 # On Windows, path typically contains backslashes. This leads |
|
955 # to an invalid search pattern '...\(' because the opening bracket |
|
956 # will be escaped. |
|
957 pattern = self.__e5project.getTranslationPattern() |
|
958 pattern = os.path.normpath(pattern) |
|
959 pattern = pattern.replace("%language%", "(.*?)") |
|
960 pattern = pattern.replace('\\', '\\\\') |
|
961 match = re.search(pattern, filename) |
|
962 if match is not None: |
|
963 return match.group(1) |
|
964 |
|
965 return None |
|
966 |
|
967 def openPOEditor(self, poFile): |
|
968 """ |
|
969 Public method to edit the given file in an external .po editor. |
|
970 |
|
971 @param poFile name of the .po file |
|
972 @type str |
|
973 """ |
|
974 editor = self.__plugin.getPreferences("TranslationsEditor") |
|
975 if poFile.endswith(".po") and editor: |
|
976 wd, _ = self.getApplication() |
|
977 started, pid = QProcess.startDetached(editor, [poFile], wd) |
|
978 if not started: |
|
979 E5MessageBox.critical( |
|
980 None, |
|
981 self.tr('Process Generation Error'), |
|
982 self.tr('The translations editor process ({0}) could' |
|
983 ' not be started.').format( |
|
984 os.path.basename(editor))) |
|
985 |
|
986 def extractMessages(self): |
|
987 """ |
|
988 Public method to extract the messages catalog template file. |
|
989 """ |
|
990 title = self.tr("Extract messages") |
|
991 if self.__ensurePybabelConfigured(): |
|
992 workdir = self.getApplication()[0] |
|
993 potFile = self.__e5project.getAbsoluteUniversalPath( |
|
994 self.getData("pybabel", "catalogFile")) |
|
995 |
|
996 try: |
|
997 potFilePath = os.path.dirname(potFile) |
|
998 os.makedirs(potFilePath) |
|
999 except EnvironmentError: |
|
1000 pass |
|
1001 |
|
1002 args = [ |
|
1003 "-F", |
|
1004 os.path.relpath( |
|
1005 self.__e5project.getAbsoluteUniversalPath( |
|
1006 self.getData("pybabel", "configFile")), |
|
1007 workdir |
|
1008 ) |
|
1009 ] |
|
1010 if self.getData("pybabel", "markersList"): |
|
1011 for marker in self.getData("pybabel", "markersList"): |
|
1012 args += ["-k", marker] |
|
1013 args += [ |
|
1014 "-o", |
|
1015 os.path.relpath(potFile, workdir), |
|
1016 "." |
|
1017 ] |
|
1018 |
|
1019 dlg = PyBabelCommandDialog( |
|
1020 self, title, |
|
1021 msgSuccess=self.tr("\nMessages extracted successfully.") |
|
1022 ) |
|
1023 res = dlg.startCommand("extract", args, workdir) |
|
1024 if res: |
|
1025 dlg.exec() |
|
1026 self.__e5project.appendFile(potFile) |
|
1027 |
|
1028 def __projectLanguageAdded(self, code): |
|
1029 """ |
|
1030 Private slot handling the addition of a new language. |
|
1031 |
|
1032 @param code language code of the new language |
|
1033 @type str |
|
1034 """ |
|
1035 title = self.tr( |
|
1036 "Initializing message catalog for '{0}'").format(code) |
|
1037 |
|
1038 if self.__ensurePybabelConfigured(): |
|
1039 workdir = self.getApplication()[0] |
|
1040 langFile = self.__e5project.getAbsoluteUniversalPath( |
|
1041 self.__e5project.getTranslationPattern().replace( |
|
1042 "%language%", code)) |
|
1043 potFile = self.__e5project.getAbsoluteUniversalPath( |
|
1044 self.getData("pybabel", "catalogFile")) |
|
1045 |
|
1046 args = [ |
|
1047 "--domain={0}".format(self.getData("pybabel", "domain")), |
|
1048 "--input-file={0}".format(os.path.relpath(potFile, workdir)), |
|
1049 "--output-file={0}".format(os.path.relpath(langFile, workdir)), |
|
1050 "--locale={0}".format(code), |
|
1051 ] |
|
1052 |
|
1053 dlg = PyBabelCommandDialog( |
|
1054 self, title, |
|
1055 msgSuccess=self.tr( |
|
1056 "\nMessage catalog initialized successfully.") |
|
1057 ) |
|
1058 res = dlg.startCommand("init", args, workdir) |
|
1059 if res: |
|
1060 dlg.exec() |
|
1061 |
|
1062 self.__e5project.appendFile(langFile) |
|
1063 |
|
1064 def compileCatalogs(self, filenames): |
|
1065 """ |
|
1066 Public method to compile the message catalogs. |
|
1067 |
|
1068 @param filenames list of filenames (not used) |
|
1069 @type list of str |
|
1070 """ |
|
1071 title = self.tr("Compiling message catalogs") |
|
1072 |
|
1073 if self.__ensurePybabelConfigured(): |
|
1074 workdir = self.getApplication()[0] |
|
1075 translationsDirectory = self.__e5project.getAbsoluteUniversalPath( |
|
1076 self.getData("pybabel", "translationsDirectory")) |
|
1077 |
|
1078 args = [ |
|
1079 "--domain={0}".format(self.getData("pybabel", "domain")), |
|
1080 "--directory={0}".format( |
|
1081 os.path.relpath(translationsDirectory, workdir)), |
|
1082 "--use-fuzzy", |
|
1083 "--statistics", |
|
1084 ] |
|
1085 |
|
1086 dlg = PyBabelCommandDialog( |
|
1087 self, title, |
|
1088 msgSuccess=self.tr("\nMessage catalogs compiled successfully.") |
|
1089 ) |
|
1090 res = dlg.startCommand("compile", args, workdir) |
|
1091 if res: |
|
1092 dlg.exec() |
|
1093 |
|
1094 for entry in os.walk(translationsDirectory): |
|
1095 for fileName in entry[2]: |
|
1096 fullName = os.path.join(entry[0], fileName) |
|
1097 if fullName.endswith('.mo'): |
|
1098 self.__e5project.appendFile(fullName) |
|
1099 |
|
1100 def compileSelectedCatalogs(self, filenames): |
|
1101 """ |
|
1102 Public method to update the message catalogs. |
|
1103 |
|
1104 @param filenames list of file names |
|
1105 @type list of str |
|
1106 """ |
|
1107 title = self.tr("Compiling message catalogs") |
|
1108 |
|
1109 locales = {self.__getLocale(f) for f in filenames} |
|
1110 |
|
1111 if len(locales) == 0: |
|
1112 E5MessageBox.warning( |
|
1113 self.__ui, |
|
1114 title, |
|
1115 self.tr('No locales detected. Aborting...')) |
|
1116 return |
|
1117 |
|
1118 if self.__ensurePybabelConfigured(): |
|
1119 workdir = self.getApplication()[0] |
|
1120 translationsDirectory = self.__e5project.getAbsoluteUniversalPath( |
|
1121 self.getData("pybabel", "translationsDirectory")) |
|
1122 |
|
1123 argsList = [] |
|
1124 for loc in locales: |
|
1125 argsList.append([ |
|
1126 "compile", |
|
1127 "--domain={0}".format(self.getData("pybabel", "domain")), |
|
1128 "--directory={0}".format( |
|
1129 os.path.relpath(translationsDirectory, workdir)), |
|
1130 "--use-fuzzy", |
|
1131 "--statistics", |
|
1132 "--locale={0}".format(loc), |
|
1133 ]) |
|
1134 |
|
1135 dlg = PyBabelCommandDialog( |
|
1136 self, title=title, |
|
1137 msgSuccess=self.tr("\nMessage catalogs compiled successfully.") |
|
1138 ) |
|
1139 res = dlg.startBatchCommand(argsList, workdir) |
|
1140 if res: |
|
1141 dlg.exec() |
|
1142 |
|
1143 for entry in os.walk(translationsDirectory): |
|
1144 for fileName in entry[2]: |
|
1145 fullName = os.path.join(entry[0], fileName) |
|
1146 if fullName.endswith('.mo'): |
|
1147 self.__e5project.appendFile(fullName) |
|
1148 |
|
1149 def updateCatalogs(self, filenames, withObsolete=False): |
|
1150 """ |
|
1151 Public method to update the message catalogs. |
|
1152 |
|
1153 @param filenames list of filenames (not used) |
|
1154 @type list of str |
|
1155 @param withObsolete flag indicating to keep obsolete translations |
|
1156 @type bool |
|
1157 """ |
|
1158 title = self.tr("Updating message catalogs") |
|
1159 |
|
1160 if self.__ensurePybabelConfigured(): |
|
1161 workdir = self.getApplication()[0] |
|
1162 translationsDirectory = self.__e5project.getAbsoluteUniversalPath( |
|
1163 self.getData("pybabel", "translationsDirectory")) |
|
1164 potFile = self.__e5project.getAbsoluteUniversalPath( |
|
1165 self.getData("pybabel", "catalogFile")) |
|
1166 |
|
1167 args = [ |
|
1168 "--domain={0}".format(self.getData("pybabel", "domain")), |
|
1169 "--input-file={0}".format(os.path.relpath(potFile, workdir)), |
|
1170 "--output-dir={0}".format( |
|
1171 os.path.relpath(translationsDirectory, workdir)), |
|
1172 ] |
|
1173 if not withObsolete: |
|
1174 args.append("--ignore-obsolete") |
|
1175 |
|
1176 dlg = PyBabelCommandDialog( |
|
1177 self, title, |
|
1178 msgSuccess=self.tr("\nMessage catalogs updated successfully.") |
|
1179 ) |
|
1180 res = dlg.startCommand("update", args, workdir) |
|
1181 if res: |
|
1182 dlg.exec() |
|
1183 |
|
1184 def updateCatalogsObsolete(self, filenames): |
|
1185 """ |
|
1186 Public method to update the message catalogs keeping obsolete |
|
1187 translations. |
|
1188 |
|
1189 @param filenames list of filenames (not used) |
|
1190 @type list of str |
|
1191 """ |
|
1192 self.updateCatalogs(filenames, withObsolete=True) |
|
1193 |
|
1194 def updateSelectedCatalogs(self, filenames, withObsolete=False): |
|
1195 """ |
|
1196 Public method to update the selected message catalogs. |
|
1197 |
|
1198 @param filenames list of filenames |
|
1199 @type list of str |
|
1200 @param withObsolete flag indicating to keep obsolete translations |
|
1201 @type bool |
|
1202 """ |
|
1203 title = self.tr("Updating message catalogs") |
|
1204 |
|
1205 locales = {self.__getLocale(f) for f in filenames} |
|
1206 |
|
1207 if len(locales) == 0: |
|
1208 E5MessageBox.warning( |
|
1209 self.__ui, |
|
1210 title, |
|
1211 self.tr('No locales detected. Aborting...')) |
|
1212 return |
|
1213 |
|
1214 if self.__ensurePybabelConfigured(): |
|
1215 workdir = self.getApplication()[0] |
|
1216 translationsDirectory = self.__e5project.getAbsoluteUniversalPath( |
|
1217 self.getData("pybabel", "translationsDirectory")) |
|
1218 potFile = self.__e5project.getAbsoluteUniversalPath( |
|
1219 self.getData("pybabel", "catalogFile")) |
|
1220 argsList = [] |
|
1221 for loc in locales: |
|
1222 args = [ |
|
1223 "update", |
|
1224 "--domain={0}".format(self.getData("pybabel", "domain")), |
|
1225 "--input-file={0}".format( |
|
1226 os.path.relpath(potFile, workdir)), |
|
1227 "--output-dir={0}".format( |
|
1228 os.path.relpath(translationsDirectory, workdir)), |
|
1229 "--locale={0}".format(loc), |
|
1230 ] |
|
1231 if not withObsolete: |
|
1232 args.append("--ignore-obsolete") |
|
1233 argsList.append(args) |
|
1234 |
|
1235 dlg = PyBabelCommandDialog( |
|
1236 self, title=title, |
|
1237 msgSuccess=self.tr("\nMessage catalogs updated successfully.") |
|
1238 ) |
|
1239 res = dlg.startBatchCommand(argsList, workdir) |
|
1240 if res: |
|
1241 dlg.exec() |
|
1242 |
|
1243 def updateSelectedCatalogsObsolete(self, filenames): |
|
1244 """ |
|
1245 Public method to update the message catalogs keeping obsolete |
|
1246 translations. |
|
1247 |
|
1248 @param filenames list of filenames (not used) |
|
1249 @type list of str |
|
1250 """ |
|
1251 self.updateSelectedCatalogs(filenames, withObsolete=True) |
|