You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

compilemessages.py 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import codecs
  2. import concurrent.futures
  3. import glob
  4. import os
  5. from django.core.management.base import BaseCommand, CommandError
  6. from django.core.management.utils import find_command, popen_wrapper
  7. def has_bom(fn):
  8. with open(fn, 'rb') as f:
  9. sample = f.read(4)
  10. return sample.startswith((codecs.BOM_UTF8, codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE))
  11. def is_writable(path):
  12. # Known side effect: updating file access/modified time to current time if
  13. # it is writable.
  14. try:
  15. with open(path, 'a'):
  16. os.utime(path, None)
  17. except (IOError, OSError):
  18. return False
  19. return True
  20. class Command(BaseCommand):
  21. help = 'Compiles .po files to .mo files for use with builtin gettext support.'
  22. requires_system_checks = False
  23. program = 'msgfmt'
  24. program_options = ['--check-format']
  25. def add_arguments(self, parser):
  26. parser.add_argument(
  27. '--locale', '-l', action='append', default=[],
  28. help='Locale(s) to process (e.g. de_AT). Default is to process all. '
  29. 'Can be used multiple times.',
  30. )
  31. parser.add_argument(
  32. '--exclude', '-x', action='append', default=[],
  33. help='Locales to exclude. Default is none. Can be used multiple times.',
  34. )
  35. parser.add_argument(
  36. '--use-fuzzy', '-f', dest='fuzzy', action='store_true',
  37. help='Use fuzzy translations.',
  38. )
  39. def handle(self, **options):
  40. locale = options['locale']
  41. exclude = options['exclude']
  42. self.verbosity = options['verbosity']
  43. if options['fuzzy']:
  44. self.program_options = self.program_options + ['-f']
  45. if find_command(self.program) is None:
  46. raise CommandError("Can't find %s. Make sure you have GNU gettext "
  47. "tools 0.15 or newer installed." % self.program)
  48. basedirs = [os.path.join('conf', 'locale'), 'locale']
  49. if os.environ.get('DJANGO_SETTINGS_MODULE'):
  50. from django.conf import settings
  51. basedirs.extend(settings.LOCALE_PATHS)
  52. # Walk entire tree, looking for locale directories
  53. for dirpath, dirnames, filenames in os.walk('.', topdown=True):
  54. for dirname in dirnames:
  55. if dirname == 'locale':
  56. basedirs.append(os.path.join(dirpath, dirname))
  57. # Gather existing directories.
  58. basedirs = set(map(os.path.abspath, filter(os.path.isdir, basedirs)))
  59. if not basedirs:
  60. raise CommandError("This script should be run from the Django Git "
  61. "checkout or your project or app tree, or with "
  62. "the settings module specified.")
  63. # Build locale list
  64. all_locales = []
  65. for basedir in basedirs:
  66. locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % basedir))
  67. all_locales.extend(map(os.path.basename, locale_dirs))
  68. # Account for excluded locales
  69. locales = locale or all_locales
  70. locales = set(locales).difference(exclude)
  71. self.has_errors = False
  72. for basedir in basedirs:
  73. if locales:
  74. dirs = [os.path.join(basedir, l, 'LC_MESSAGES') for l in locales]
  75. else:
  76. dirs = [basedir]
  77. locations = []
  78. for ldir in dirs:
  79. for dirpath, dirnames, filenames in os.walk(ldir):
  80. locations.extend((dirpath, f) for f in filenames if f.endswith('.po'))
  81. if locations:
  82. self.compile_messages(locations)
  83. if self.has_errors:
  84. raise CommandError('compilemessages generated one or more errors.')
  85. def compile_messages(self, locations):
  86. """
  87. Locations is a list of tuples: [(directory, file), ...]
  88. """
  89. with concurrent.futures.ThreadPoolExecutor() as executor:
  90. futures = []
  91. for i, (dirpath, f) in enumerate(locations):
  92. if self.verbosity > 0:
  93. self.stdout.write('processing file %s in %s\n' % (f, dirpath))
  94. po_path = os.path.join(dirpath, f)
  95. if has_bom(po_path):
  96. self.stderr.write(
  97. 'The %s file has a BOM (Byte Order Mark). Django only '
  98. 'supports .po files encoded in UTF-8 and without any BOM.' % po_path
  99. )
  100. self.has_errors = True
  101. continue
  102. base_path = os.path.splitext(po_path)[0]
  103. # Check writability on first location
  104. if i == 0 and not is_writable(base_path + '.mo'):
  105. self.stderr.write(
  106. 'The po files under %s are in a seemingly not writable location. '
  107. 'mo files will not be updated/created.' % dirpath
  108. )
  109. self.has_errors = True
  110. return
  111. args = [self.program] + self.program_options + [
  112. '-o', base_path + '.mo', base_path + '.po'
  113. ]
  114. futures.append(executor.submit(popen_wrapper, args))
  115. for future in concurrent.futures.as_completed(futures):
  116. output, errors, status = future.result()
  117. if status:
  118. if self.verbosity > 0:
  119. if errors:
  120. self.stderr.write("Execution of %s failed: %s" % (self.program, errors))
  121. else:
  122. self.stderr.write("Execution of %s failed" % self.program)
  123. self.has_errors = True