web/lib/django/core/management/commands/makemessages.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 import fnmatch
       
     2 import glob
       
     3 import os
       
     4 import re
       
     5 import sys
       
     6 from itertools import dropwhile
       
     7 from optparse import make_option
       
     8 from subprocess import PIPE, Popen
       
     9 
       
    10 from django.core.management.base import CommandError, BaseCommand
       
    11 from django.utils.text import get_text_list
       
    12 
       
    13 pythonize_re = re.compile(r'(?:^|\n)\s*//')
       
    14 plural_forms_re = re.compile(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL)
       
    15 
       
    16 def handle_extensions(extensions=('html',)):
       
    17     """
       
    18     organizes multiple extensions that are separated with commas or passed by
       
    19     using --extension/-e multiple times.
       
    20 
       
    21     for example: running 'django-admin makemessages -e js,txt -e xhtml -a'
       
    22     would result in a extension list: ['.js', '.txt', '.xhtml']
       
    23 
       
    24     >>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
       
    25     ['.html', '.js']
       
    26     >>> handle_extensions(['.html, txt,.tpl'])
       
    27     ['.html', '.tpl', '.txt']
       
    28     """
       
    29     ext_list = []
       
    30     for ext in extensions:
       
    31         ext_list.extend(ext.replace(' ','').split(','))
       
    32     for i, ext in enumerate(ext_list):
       
    33         if not ext.startswith('.'):
       
    34             ext_list[i] = '.%s' % ext_list[i]
       
    35 
       
    36     # we don't want *.py files here because of the way non-*.py files
       
    37     # are handled in make_messages() (they are copied to file.ext.py files to
       
    38     # trick xgettext to parse them as Python files)
       
    39     return set([x for x in ext_list if x != '.py'])
       
    40 
       
    41 def _popen(cmd):
       
    42     """
       
    43     Friendly wrapper around Popen for Windows
       
    44     """
       
    45     p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True)
       
    46     return p.communicate()
       
    47 
       
    48 def walk(root, topdown=True, onerror=None, followlinks=False):
       
    49     """
       
    50     A version of os.walk that can follow symlinks for Python < 2.6
       
    51     """
       
    52     for dirpath, dirnames, filenames in os.walk(root, topdown, onerror):
       
    53         yield (dirpath, dirnames, filenames)
       
    54         if followlinks:
       
    55             for d in dirnames:
       
    56                 p = os.path.join(dirpath, d)
       
    57                 if os.path.islink(p):
       
    58                     for link_dirpath, link_dirnames, link_filenames in walk(p):
       
    59                         yield (link_dirpath, link_dirnames, link_filenames)
       
    60 
       
    61 def is_ignored(path, ignore_patterns):
       
    62     """
       
    63     Helper function to check if the given path should be ignored or not.
       
    64     """
       
    65     for pattern in ignore_patterns:
       
    66         if fnmatch.fnmatchcase(path, pattern):
       
    67             return True
       
    68     return False
       
    69 
       
    70 def find_files(root, ignore_patterns, verbosity, symlinks=False):
       
    71     """
       
    72     Helper function to get all files in the given root.
       
    73     """
       
    74     all_files = []
       
    75     for (dirpath, dirnames, filenames) in walk(".", followlinks=symlinks):
       
    76         for f in filenames:
       
    77             norm_filepath = os.path.normpath(os.path.join(dirpath, f))
       
    78             if is_ignored(norm_filepath, ignore_patterns):
       
    79                 if verbosity > 1:
       
    80                     sys.stdout.write('ignoring file %s in %s\n' % (f, dirpath))
       
    81             else:
       
    82                 all_files.extend([(dirpath, f)])
       
    83     all_files.sort()
       
    84     return all_files
       
    85 
       
    86 def copy_plural_forms(msgs, locale, domain, verbosity):
       
    87     """
       
    88     Copies plural forms header contents from a Django catalog of locale to
       
    89     the msgs string, inserting it at the right place. msgs should be the
       
    90     contents of a newly created .po file.
       
    91     """
       
    92     import django
       
    93     django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__)))
       
    94     if domain == 'djangojs':
       
    95         domains = ('djangojs', 'django')
       
    96     else:
       
    97         domains = ('django',)
       
    98     for domain in domains:
       
    99         django_po = os.path.join(django_dir, 'conf', 'locale', locale, 'LC_MESSAGES', '%s.po' % domain)
       
   100         if os.path.exists(django_po):
       
   101             m = plural_forms_re.search(open(django_po, 'rU').read())
       
   102             if m:
       
   103                 if verbosity > 1:
       
   104                     sys.stderr.write("copying plural forms: %s\n" % m.group('value'))
       
   105                 lines = []
       
   106                 seen = False
       
   107                 for line in msgs.split('\n'):
       
   108                     if not line and not seen:
       
   109                         line = '%s\n' % m.group('value')
       
   110                         seen = True
       
   111                     lines.append(line)
       
   112                 msgs = '\n'.join(lines)
       
   113                 break
       
   114     return msgs
       
   115 
       
   116 
       
   117 def make_messages(locale=None, domain='django', verbosity='1', all=False,
       
   118         extensions=None, symlinks=False, ignore_patterns=[]):
       
   119     """
       
   120     Uses the locale directory from the Django SVN tree or an application/
       
   121     project to process all
       
   122     """
       
   123     # Need to ensure that the i18n framework is enabled
       
   124     from django.conf import settings
       
   125     if settings.configured:
       
   126         settings.USE_I18N = True
       
   127     else:
       
   128         settings.configure(USE_I18N = True)
       
   129 
       
   130     from django.utils.translation import templatize
       
   131 
       
   132     invoked_for_django = False
       
   133     if os.path.isdir(os.path.join('conf', 'locale')):
       
   134         localedir = os.path.abspath(os.path.join('conf', 'locale'))
       
   135         invoked_for_django = True
       
   136     elif os.path.isdir('locale'):
       
   137         localedir = os.path.abspath('locale')
       
   138     else:
       
   139         raise CommandError("This script should be run from the Django SVN tree or your project or app tree. If you did indeed run it from the SVN checkout or your project or application, maybe you are just missing the conf/locale (in the django tree) or locale (for project and application) directory? It is not created automatically, you have to create it by hand if you want to enable i18n for your project or application.")
       
   140 
       
   141     if domain not in ('django', 'djangojs'):
       
   142         raise CommandError("currently makemessages only supports domains 'django' and 'djangojs'")
       
   143 
       
   144     if (locale is None and not all) or domain is None:
       
   145         # backwards compatible error message
       
   146         if not sys.argv[0].endswith("make-messages.py"):
       
   147             message = "Type '%s help %s' for usage.\n" % (os.path.basename(sys.argv[0]), sys.argv[1])
       
   148         else:
       
   149             message = "usage: make-messages.py -l <language>\n   or: make-messages.py -a\n"
       
   150         raise CommandError(message)
       
   151 
       
   152     # We require gettext version 0.15 or newer.
       
   153     output = _popen('xgettext --version')[0]
       
   154     match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output)
       
   155     if match:
       
   156         xversion = (int(match.group('major')), int(match.group('minor')))
       
   157         if xversion < (0, 15):
       
   158             raise CommandError("Django internationalization requires GNU gettext 0.15 or newer. You are using version %s, please upgrade your gettext toolset." % match.group())
       
   159 
       
   160     languages = []
       
   161     if locale is not None:
       
   162         languages.append(locale)
       
   163     elif all:
       
   164         locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir))
       
   165         languages = [os.path.basename(l) for l in locale_dirs]
       
   166 
       
   167     for locale in languages:
       
   168         if verbosity > 0:
       
   169             print "processing language", locale
       
   170         basedir = os.path.join(localedir, locale, 'LC_MESSAGES')
       
   171         if not os.path.isdir(basedir):
       
   172             os.makedirs(basedir)
       
   173 
       
   174         pofile = os.path.join(basedir, '%s.po' % domain)
       
   175         potfile = os.path.join(basedir, '%s.pot' % domain)
       
   176 
       
   177         if os.path.exists(potfile):
       
   178             os.unlink(potfile)
       
   179 
       
   180         for dirpath, file in find_files(".", ignore_patterns, verbosity, symlinks=symlinks):
       
   181             file_base, file_ext = os.path.splitext(file)
       
   182             if domain == 'djangojs' and file_ext in extensions:
       
   183                 if verbosity > 1:
       
   184                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
       
   185                 src = open(os.path.join(dirpath, file), "rU").read()
       
   186                 src = pythonize_re.sub('\n#', src)
       
   187                 thefile = '%s.py' % file
       
   188                 f = open(os.path.join(dirpath, thefile), "w")
       
   189                 try:
       
   190                     f.write(src)
       
   191                 finally:
       
   192                     f.close()
       
   193                 cmd = 'xgettext -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % (domain, os.path.join(dirpath, thefile))
       
   194                 msgs, errors = _popen(cmd)
       
   195                 if errors:
       
   196                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
       
   197                 old = '#: '+os.path.join(dirpath, thefile)[2:]
       
   198                 new = '#: '+os.path.join(dirpath, file)[2:]
       
   199                 msgs = msgs.replace(old, new)
       
   200                 if os.path.exists(potfile):
       
   201                     # Strip the header
       
   202                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
       
   203                 else:
       
   204                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
       
   205                 if msgs:
       
   206                     f = open(potfile, 'ab')
       
   207                     try:
       
   208                         f.write(msgs)
       
   209                     finally:
       
   210                         f.close()
       
   211                 os.unlink(os.path.join(dirpath, thefile))
       
   212             elif domain == 'django' and (file_ext == '.py' or file_ext in extensions):
       
   213                 thefile = file
       
   214                 if file_ext in extensions:
       
   215                     src = open(os.path.join(dirpath, file), "rU").read()
       
   216                     thefile = '%s.py' % file
       
   217                     try:
       
   218                         f = open(os.path.join(dirpath, thefile), "w")
       
   219                         try:
       
   220                             f.write(templatize(src))
       
   221                         finally:
       
   222                             f.close()
       
   223                     except SyntaxError, msg:
       
   224                         msg = "%s (file: %s)" % (msg, os.path.join(dirpath, file))
       
   225                         raise SyntaxError(msg)
       
   226                 if verbosity > 1:
       
   227                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
       
   228                 cmd = 'xgettext -d %s -L Python --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --keyword=ugettext_noop --keyword=ugettext_lazy --keyword=ungettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % (
       
   229                     domain, os.path.join(dirpath, thefile))
       
   230                 msgs, errors = _popen(cmd)
       
   231                 if errors:
       
   232                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
       
   233 
       
   234                 if thefile != file:
       
   235                     old = '#: '+os.path.join(dirpath, thefile)[2:]
       
   236                     new = '#: '+os.path.join(dirpath, file)[2:]
       
   237                     msgs = msgs.replace(old, new)
       
   238                 if os.path.exists(potfile):
       
   239                     # Strip the header
       
   240                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
       
   241                 else:
       
   242                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
       
   243                 if msgs:
       
   244                     f = open(potfile, 'ab')
       
   245                     try:
       
   246                         f.write(msgs)
       
   247                     finally:
       
   248                         f.close()
       
   249                 if thefile != file:
       
   250                     os.unlink(os.path.join(dirpath, thefile))
       
   251 
       
   252         if os.path.exists(potfile):
       
   253             msgs, errors = _popen('msguniq --to-code=utf-8 "%s"' % potfile)
       
   254             if errors:
       
   255                 raise CommandError("errors happened while running msguniq\n%s" % errors)
       
   256             f = open(potfile, 'w')
       
   257             try:
       
   258                 f.write(msgs)
       
   259             finally:
       
   260                 f.close()
       
   261             if os.path.exists(pofile):
       
   262                 msgs, errors = _popen('msgmerge -q "%s" "%s"' % (pofile, potfile))
       
   263                 if errors:
       
   264                     raise CommandError("errors happened while running msgmerge\n%s" % errors)
       
   265             elif not invoked_for_django:
       
   266                 msgs = copy_plural_forms(msgs, locale, domain, verbosity)
       
   267             f = open(pofile, 'wb')
       
   268             try:
       
   269                 f.write(msgs)
       
   270             finally:
       
   271                 f.close()
       
   272             os.unlink(potfile)
       
   273 
       
   274 
       
   275 class Command(BaseCommand):
       
   276     option_list = BaseCommand.option_list + (
       
   277         make_option('--locale', '-l', default=None, dest='locale',
       
   278             help='Creates or updates the message files only for the given locale (e.g. pt_BR).'),
       
   279         make_option('--domain', '-d', default='django', dest='domain',
       
   280             help='The domain of the message files (default: "django").'),
       
   281         make_option('--all', '-a', action='store_true', dest='all',
       
   282             default=False, help='Reexamines all source code and templates for new translation strings and updates all message files for all available languages.'),
       
   283         make_option('--extension', '-e', dest='extensions',
       
   284             help='The file extension(s) to examine (default: ".html", separate multiple extensions with commas, or use -e multiple times)',
       
   285             action='append'),
       
   286         make_option('--symlinks', '-s', action='store_true', dest='symlinks',
       
   287             default=False, help='Follows symlinks to directories when examining source code and templates for translation strings.'),
       
   288         make_option('--ignore', '-i', action='append', dest='ignore_patterns',
       
   289             default=[], metavar='PATTERN', help='Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more.'),
       
   290         make_option('--no-default-ignore', action='store_false', dest='use_default_ignore_patterns',
       
   291             default=True, help="Don't ignore the common glob-style patterns 'CVS', '.*' and '*~'."),
       
   292     )
       
   293     help = "Runs over the entire source tree of the current directory and pulls out all strings marked for translation. It creates (or updates) a message file in the conf/locale (in the django tree) or locale (for project and application) directory."
       
   294 
       
   295     requires_model_validation = False
       
   296     can_import_settings = False
       
   297 
       
   298     def handle(self, *args, **options):
       
   299         if len(args) != 0:
       
   300             raise CommandError("Command doesn't accept any arguments")
       
   301 
       
   302         locale = options.get('locale')
       
   303         domain = options.get('domain')
       
   304         verbosity = int(options.get('verbosity'))
       
   305         process_all = options.get('all')
       
   306         extensions = options.get('extensions')
       
   307         symlinks = options.get('symlinks')
       
   308         ignore_patterns = options.get('ignore_patterns')
       
   309         if options.get('use_default_ignore_patterns'):
       
   310             ignore_patterns += ['CVS', '.*', '*~']
       
   311         ignore_patterns = list(set(ignore_patterns))
       
   312 
       
   313         if domain == 'djangojs':
       
   314             extensions = handle_extensions(extensions or ['js'])
       
   315         else:
       
   316             extensions = handle_extensions(extensions or ['html'])
       
   317 
       
   318         if verbosity > 1:
       
   319             sys.stdout.write('examining files with the extensions: %s\n' % get_text_list(list(extensions), 'and'))
       
   320 
       
   321         make_messages(locale, domain, verbosity, process_all, extensions, symlinks, ignore_patterns)