| 1 | import os, commands, re |
|---|
| 2 | from django.contrib.contenttypes.models import ContentType |
|---|
| 3 | from django.conf import settings |
|---|
| 4 | from translations.lib.types import (TransManagerMixin, TransManagerError) |
|---|
| 5 | from translations.models import POFile, Language |
|---|
| 6 | from translations.lib.utils import (run_command, CommandError) |
|---|
| 7 | |
|---|
| 8 | class POTStatsError(Exception): |
|---|
| 9 | |
|---|
| 10 | def __init__(self, language): |
|---|
| 11 | self.language = language |
|---|
| 12 | |
|---|
| 13 | def __str__(self): |
|---|
| 14 | return "Could not calculate the statistics using the '%s' " \ |
|---|
| 15 | "language." % (self.language) |
|---|
| 16 | |
|---|
| 17 | class FileFilterError(Exception): |
|---|
| 18 | |
|---|
| 19 | def __str__(self): |
|---|
| 20 | return "The file filter should allows the POTFILES.in file" \ |
|---|
| 21 | " for intltool POT-based projects." |
|---|
| 22 | |
|---|
| 23 | class POTManager(TransManagerMixin): |
|---|
| 24 | """ A browser class for POT files. """ |
|---|
| 25 | |
|---|
| 26 | def __init__(self, file_set, path, source_lang, file_filter, |
|---|
| 27 | filepath=None): |
|---|
| 28 | self.file_set = file_set |
|---|
| 29 | if filepath is None: |
|---|
| 30 | filepath = path |
|---|
| 31 | self.path = filepath |
|---|
| 32 | self.source_lang = source_lang |
|---|
| 33 | self.file_filter = file_filter |
|---|
| 34 | self.msgmerge_path = os.path.join(settings.MSGMERGE_DIR, |
|---|
| 35 | os.path.basename(path)) |
|---|
| 36 | |
|---|
| 37 | def get_file_path(self, filename, is_msgmerged=False): |
|---|
| 38 | # All the files should be in the file_set, except the intltool |
|---|
| 39 | # POT file that is created by the system |
|---|
| 40 | if filename in self.file_set or \ |
|---|
| 41 | filename.endswith('.pot') and is_msgmerged: |
|---|
| 42 | if is_msgmerged: |
|---|
| 43 | file_path = os.path.join(self.msgmerge_path, filename) |
|---|
| 44 | else: |
|---|
| 45 | file_path = os.path.join(self.path, filename) |
|---|
| 46 | else: |
|---|
| 47 | raise IOError("File not found.") |
|---|
| 48 | return file_path |
|---|
| 49 | |
|---|
| 50 | def get_file_content(self, filename, is_msgmerged=False): |
|---|
| 51 | file_path = self.get_file_path(filename, is_msgmerged) |
|---|
| 52 | filef = file(file_path, 'rb') |
|---|
| 53 | file_content = filef.read() |
|---|
| 54 | filef.close() |
|---|
| 55 | return file_content |
|---|
| 56 | |
|---|
| 57 | def get_po_files(self): |
|---|
| 58 | """ Return a list of PO filenames """ |
|---|
| 59 | |
|---|
| 60 | po_files = [] |
|---|
| 61 | for filename in self.file_set: |
|---|
| 62 | if filename.endswith('.po'): |
|---|
| 63 | po_files.append(filename) |
|---|
| 64 | po_files.sort() |
|---|
| 65 | return po_files |
|---|
| 66 | |
|---|
| 67 | def get_langfiles(self, lang): |
|---|
| 68 | """ Return a list with the PO filenames for a specificy language """ |
|---|
| 69 | |
|---|
| 70 | files=[] |
|---|
| 71 | for filepath in self.get_po_files(): |
|---|
| 72 | if self.guess_language(filepath) == lang: |
|---|
| 73 | files.append(filepath) |
|---|
| 74 | return files |
|---|
| 75 | |
|---|
| 76 | def guess_language(self, filepath): |
|---|
| 77 | """ Guess a language from a filepath """ |
|---|
| 78 | |
|---|
| 79 | if 'LC_MESSAGES' in filepath: |
|---|
| 80 | fp = filepath.split('LC_MESSAGES') |
|---|
| 81 | return os.path.basename(fp[0][:-1:]) |
|---|
| 82 | else: |
|---|
| 83 | return os.path.basename(filepath[:-3:]) |
|---|
| 84 | |
|---|
| 85 | def get_langs(self): |
|---|
| 86 | """ Return all langs tha have a po file for a object """ |
|---|
| 87 | |
|---|
| 88 | langs = [] |
|---|
| 89 | for filepath in self.get_po_files(): |
|---|
| 90 | langs.append(self.guess_language(filepath)) |
|---|
| 91 | langs.sort() |
|---|
| 92 | return langs |
|---|
| 93 | |
|---|
| 94 | |
|---|
| 95 | def po_file_stats(self, pofile): |
|---|
| 96 | """ Calculate stats for a POT/PO file """ |
|---|
| 97 | error = False |
|---|
| 98 | pofile = os.path.join(self.msgmerge_path, pofile) |
|---|
| 99 | |
|---|
| 100 | command = "LC_ALL=C LANG=C LANGUAGE=C msgfmt --statistics" \ |
|---|
| 101 | " -o /dev/null %s" % pofile |
|---|
| 102 | (error, output) = commands.getstatusoutput(command) |
|---|
| 103 | |
|---|
| 104 | if error: |
|---|
| 105 | error = True |
|---|
| 106 | |
|---|
| 107 | r_tr = re.search(r"([0-9]+) translated", output) |
|---|
| 108 | r_un = re.search(r"([0-9]+) untranslated", output) |
|---|
| 109 | r_fz = re.search(r"([0-9]+) fuzzy", output) |
|---|
| 110 | |
|---|
| 111 | if r_tr: translated = r_tr.group(1) |
|---|
| 112 | else: translated = 0 |
|---|
| 113 | if r_un: untranslated = r_un.group(1) |
|---|
| 114 | else: untranslated = 0 |
|---|
| 115 | if r_fz: fuzzy = r_fz.group(1) |
|---|
| 116 | else: fuzzy = 0 |
|---|
| 117 | |
|---|
| 118 | return {'translated' : int(translated), |
|---|
| 119 | 'fuzzy' : int(fuzzy), |
|---|
| 120 | 'untranslated' : int(untranslated), |
|---|
| 121 | 'error' : error,} |
|---|
| 122 | |
|---|
| 123 | def calculate_file_stats(self, filename, try_to_merge): |
|---|
| 124 | """ |
|---|
| 125 | Return the statistics of a specificy file for an object after |
|---|
| 126 | merging the file with the source translation file (POT), if possible. |
|---|
| 127 | """ |
|---|
| 128 | # We might want to skip the msgmerge setting try_to_merge as False |
|---|
| 129 | if try_to_merge: |
|---|
| 130 | source_file = self.get_source_file_for_pofile(filename) |
|---|
| 131 | (is_msgmerged, file_path) = self.msgmerge(filename, source_file) |
|---|
| 132 | else: |
|---|
| 133 | is_msgmerged=False |
|---|
| 134 | file_path = os.path.join(self.path, filename) |
|---|
| 135 | |
|---|
| 136 | #Copy the current file (non-msgmerged) to the static dir |
|---|
| 137 | if not is_msgmerged: |
|---|
| 138 | self.copy_file_to_static_dir(filename) |
|---|
| 139 | |
|---|
| 140 | postats = self.po_file_stats(file_path) |
|---|
| 141 | |
|---|
| 142 | return {'trans': postats['translated'], |
|---|
| 143 | 'fuzzy': postats['fuzzy'], |
|---|
| 144 | 'untrans': postats['untranslated'], |
|---|
| 145 | 'error': postats['error'], |
|---|
| 146 | 'is_msgmerged': is_msgmerged} |
|---|
| 147 | |
|---|
| 148 | def create_lang_stats(self, lang, object, try_to_merge=True): |
|---|
| 149 | """Set the statistics of a specificy language for an object.""" |
|---|
| 150 | |
|---|
| 151 | for filename in self.get_langfiles(lang): |
|---|
| 152 | self.create_file_stats(filename, object, try_to_merge) |
|---|
| 153 | |
|---|
| 154 | def create_file_stats(self, filename, object, try_to_merge=True): |
|---|
| 155 | """Set the statistics of a specificy file for an object.""" |
|---|
| 156 | lang_code = self.guess_language(filename) |
|---|
| 157 | try: |
|---|
| 158 | stats = self.calculate_file_stats(filename, try_to_merge) |
|---|
| 159 | ctype = ContentType.objects.get_for_model(object) |
|---|
| 160 | s = POFile.objects.get(object_id=object.id, content_type=ctype, |
|---|
| 161 | filename=filename) |
|---|
| 162 | if not s.language: |
|---|
| 163 | try: |
|---|
| 164 | l = Language.objects.by_code_or_alias(code=lang_code) |
|---|
| 165 | s.language=l |
|---|
| 166 | except Language.DoesNotExist: |
|---|
| 167 | pass |
|---|
| 168 | except POTStatsError: |
|---|
| 169 | # TODO: It should probably be raised when a checkout of a |
|---|
| 170 | # module has a problem. Needs to decide what to do when it |
|---|
| 171 | # happens |
|---|
| 172 | pass |
|---|
| 173 | except POFile.DoesNotExist: |
|---|
| 174 | try: |
|---|
| 175 | l = Language.objects.by_code_or_alias(code=lang_code) |
|---|
| 176 | except Language.DoesNotExist: |
|---|
| 177 | l = None |
|---|
| 178 | s = POFile.objects.create(language=l, filename=filename, |
|---|
| 179 | object=object) |
|---|
| 180 | s.set_stats(trans=stats['trans'], fuzzy=stats['fuzzy'], |
|---|
| 181 | untrans=stats['untrans'], error=stats['error']) |
|---|
| 182 | s.is_msgmerged = stats['is_msgmerged'] |
|---|
| 183 | s.language_code = lang_code |
|---|
| 184 | return s.save() |
|---|
| 185 | |
|---|
| 186 | def stats_for_lang_object(self, lang, object): |
|---|
| 187 | """Return statistics for an object in a specific language.""" |
|---|
| 188 | try: |
|---|
| 189 | ctype = ContentType.objects.get_for_model(object) |
|---|
| 190 | return POFile.objects.filter(language=lang, content_type=ctype, |
|---|
| 191 | object_id=object.id)[0] |
|---|
| 192 | except IndexError: |
|---|
| 193 | return None |
|---|
| 194 | |
|---|
| 195 | def get_stats(self, object): |
|---|
| 196 | """ Return a list of statistics of languages for an object.""" |
|---|
| 197 | return POFile.objects.by_object_total(object) |
|---|
| 198 | |
|---|
| 199 | def delete_stats_for_object(self, object): |
|---|
| 200 | """ Delete all lang statistics of an object.""" |
|---|
| 201 | ctype = ContentType.objects.get_for_model(object) |
|---|
| 202 | POFile.objects.filter(object_id=object.id, content_type=ctype).delete() |
|---|
| 203 | |
|---|
| 204 | def set_source_stats(self, object, is_msgmerged): |
|---|
| 205 | """Set the source file (pot) in the database""" |
|---|
| 206 | |
|---|
| 207 | ctype = ContentType.objects.get_for_model(object) |
|---|
| 208 | potfiles=self.get_source_files() |
|---|
| 209 | for potfile in potfiles: |
|---|
| 210 | p, created = POFile.objects.get_or_create(filename=potfile, |
|---|
| 211 | is_pot=True, |
|---|
| 212 | content_type=ctype, |
|---|
| 213 | object_id=object.id, |
|---|
| 214 | is_msgmerged=is_msgmerged) |
|---|
| 215 | stats = self.po_file_stats(potfile) |
|---|
| 216 | p.set_stats(trans=stats['translated'], |
|---|
| 217 | fuzzy=stats['fuzzy'], |
|---|
| 218 | untrans=stats['untranslated'], |
|---|
| 219 | error=stats['error']) |
|---|
| 220 | |
|---|
| 221 | p.save() |
|---|
| 222 | |
|---|
| 223 | def get_source_stats(self, object): |
|---|
| 224 | """ |
|---|
| 225 | Return a list of the source file (pot) statistics from the database |
|---|
| 226 | """ |
|---|
| 227 | try: |
|---|
| 228 | ctype = ContentType.objects.get_for_model(object) |
|---|
| 229 | return POFile.objects.filter(object_id=object.id, |
|---|
| 230 | content_type=ctype, is_pot=True) |
|---|
| 231 | except POFile.DoesNotExist: |
|---|
| 232 | return None |
|---|
| 233 | |
|---|
| 234 | def get_source_files(self): |
|---|
| 235 | """ |
|---|
| 236 | Return a list with the source files (pot) paths |
|---|
| 237 | |
|---|
| 238 | Try to find it in the file_set passed to the PO file instace. |
|---|
| 239 | If it still fauls, try to find the POT file in the filesystem. |
|---|
| 240 | """ |
|---|
| 241 | pofiles=[] |
|---|
| 242 | for filename in self.file_set: |
|---|
| 243 | if filename.endswith('.pot'): |
|---|
| 244 | pofiles.append(filename) |
|---|
| 245 | |
|---|
| 246 | # If there is no POT in the file_set, try to find it in |
|---|
| 247 | # the file system |
|---|
| 248 | if not pofiles: |
|---|
| 249 | filename = self.get_intltool_source_file(self.msgmerge_path) |
|---|
| 250 | if filename: |
|---|
| 251 | pofiles.append(filename) |
|---|
| 252 | |
|---|
| 253 | return pofiles |
|---|
| 254 | |
|---|
| 255 | def get_intltool_source_file(self, po_dir): |
|---|
| 256 | """Return the POT file that might be created by intltool""" |
|---|
| 257 | for root, dirs, files in os.walk(po_dir): |
|---|
| 258 | for filename in files: |
|---|
| 259 | if filename.endswith('.pot'): |
|---|
| 260 | # Get the relative path |
|---|
| 261 | rel_path = root.split(os.path.basename(self.path))[1] |
|---|
| 262 | # Return the relative path of the POT file without |
|---|
| 263 | # the / in the start of the POT file path |
|---|
| 264 | return os.path.join(rel_path, filename)[1:] |
|---|
| 265 | |
|---|
| 266 | def get_source_file_for_pofile(self, filename): |
|---|
| 267 | """ |
|---|
| 268 | Find the related source file (POT) for a pofile when it has multiple |
|---|
| 269 | source files. |
|---|
| 270 | |
|---|
| 271 | This method gets a filename as parameter and tries to discover the |
|---|
| 272 | related POT file using two methods: |
|---|
| 273 | 1. Trying to find a POT file with the same base path that the pofile. |
|---|
| 274 | Example: /foo/bar.pot and /foo/baz.po match on this method. |
|---|
| 275 | |
|---|
| 276 | 2. Trying to find a POT file with the same domain that the pofile in any |
|---|
| 277 | directory. |
|---|
| 278 | Example: /foo/bar.pot and /foo/baz/bar.po match on this method. |
|---|
| 279 | The domain in this case is 'bar'. |
|---|
| 280 | |
|---|
| 281 | If no POT is found the method returns None. |
|---|
| 282 | """ |
|---|
| 283 | |
|---|
| 284 | # For filename='/foo/bar.po' |
|---|
| 285 | fb = os.path.basename(filename) # 'bar.po' |
|---|
| 286 | fp = filename.split(fb)[0] # '/foo/' |
|---|
| 287 | |
|---|
| 288 | source_files = self.get_source_files() |
|---|
| 289 | |
|---|
| 290 | # Find the POT with the same domain or path that the filename, |
|---|
| 291 | # if the component has more that one POT file |
|---|
| 292 | if len(source_files) > 1: |
|---|
| 293 | for source in source_files: |
|---|
| 294 | sb = os.path.basename(source)[:-1] # *.po instead *.pot |
|---|
| 295 | pb = source.split(sb)[0] |
|---|
| 296 | if pb==fp or sb==fb: |
|---|
| 297 | return source |
|---|
| 298 | elif len(source_files) == 1: |
|---|
| 299 | return source_files[0] |
|---|
| 300 | else: |
|---|
| 301 | return None |
|---|
| 302 | |
|---|
| 303 | def copy_file_to_static_dir(self, filename): |
|---|
| 304 | """Copy a file to the destination""" |
|---|
| 305 | import shutil |
|---|
| 306 | |
|---|
| 307 | dest = os.path.join(self.msgmerge_path, filename) |
|---|
| 308 | |
|---|
| 309 | if not os.path.exists(os.path.dirname(dest)): |
|---|
| 310 | os.makedirs(os.path.dirname(dest)) |
|---|
| 311 | |
|---|
| 312 | shutil.copyfile(os.path.join(self.path, filename), dest) |
|---|
| 313 | |
|---|
| 314 | def msgmerge(self, pofile, potfile): |
|---|
| 315 | """ |
|---|
| 316 | Merge two files and save the output at the settings.MSGMERGE_DIR. |
|---|
| 317 | In case that error, copy the source file (pofile) to the |
|---|
| 318 | destination without merging. |
|---|
| 319 | """ |
|---|
| 320 | is_msgmerged = True |
|---|
| 321 | outpo = os.path.join(self.msgmerge_path, pofile) |
|---|
| 322 | |
|---|
| 323 | try: |
|---|
| 324 | # TODO: Find a library to avoid call msgmerge by command |
|---|
| 325 | command = "msgmerge -o %(outpo)s %(pofile)s %(potfile)s" % { |
|---|
| 326 | 'outpo' : outpo, |
|---|
| 327 | 'pofile' : os.path.join(self.path, pofile), |
|---|
| 328 | 'potfile' : os.path.join(self.msgmerge_path, potfile),} |
|---|
| 329 | |
|---|
| 330 | (error, output) = commands.getstatusoutput(command) |
|---|
| 331 | except: |
|---|
| 332 | error = True |
|---|
| 333 | |
|---|
| 334 | if error: |
|---|
| 335 | # TODO: Log this. output var can be used. |
|---|
| 336 | is_msgmerged = False |
|---|
| 337 | |
|---|
| 338 | return (is_msgmerged, outpo) |
|---|
| 339 | |
|---|
| 340 | def guess_po_dir(self): |
|---|
| 341 | """ Guess the po/ diretory to run intltool """ |
|---|
| 342 | for filename in self.file_set: |
|---|
| 343 | if 'POTFILES.in' in filename: |
|---|
| 344 | if self.file_filter: |
|---|
| 345 | if re.compile(self.file_filter).match(filename): |
|---|
| 346 | return os.path.join(self.path, |
|---|
| 347 | os.path.dirname(filename)) |
|---|
| 348 | raise FileFilterError |
|---|
| 349 | |
|---|
| 350 | def intltool_update(self): |
|---|
| 351 | """ |
|---|
| 352 | Create a new POT file using "intltool-update -p" from the |
|---|
| 353 | source files. Return False if it fails. |
|---|
| 354 | """ |
|---|
| 355 | po_dir = self.guess_po_dir() |
|---|
| 356 | try: |
|---|
| 357 | command = "cd \"%(dir)s\" && rm -f missing notexist && " \ |
|---|
| 358 | "intltool-update -p" % { "dir" : po_dir, } |
|---|
| 359 | (error, output) = commands.getstatusoutput(command) |
|---|
| 360 | except: |
|---|
| 361 | error = True |
|---|
| 362 | |
|---|
| 363 | # Copy the potfile if it exist to the merged files directory |
|---|
| 364 | potfile = self.get_intltool_source_file(po_dir) |
|---|
| 365 | if potfile: |
|---|
| 366 | self.copy_file_to_static_dir(potfile) |
|---|
| 367 | |
|---|
| 368 | if error: |
|---|
| 369 | # TODO: Log this. output var can be used. |
|---|
| 370 | return False |
|---|
| 371 | |
|---|
| 372 | return True |
|---|
| 373 | |
|---|
| 374 | def msgfmt_check(self, po_contents): |
|---|
| 375 | """ |
|---|
| 376 | Run a `msgfmt -c` on a file (file object). |
|---|
| 377 | Raises a ValueError in case the file has errors. |
|---|
| 378 | """ |
|---|
| 379 | try: |
|---|
| 380 | p = run_command('msgfmt -o /dev/null -c -', _input=po_contents) |
|---|
| 381 | except CommandError: |
|---|
| 382 | # TODO: Figure out why gettext is not working here |
|---|
| 383 | raise ValueError, "Your file does not" \ |
|---|
| 384 | " pass by the check for correctness" \ |
|---|
| 385 | " (msgfmt -c). Please run this command" \ |
|---|
| 386 | " on your system to see the errors." |
|---|