app/django/core/management/commands/makemessages.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     1 import re
       
     2 import os
       
     3 import sys
       
     4 import warnings
       
     5 from itertools import dropwhile
       
     6 from optparse import make_option
       
     7 
       
     8 from django.core.management.base import CommandError, BaseCommand
       
     9 
       
    10 try:
       
    11     set
       
    12 except NameError:
       
    13     from sets import Set as set     # For Python 2.3
       
    14 
       
    15 # Intentionally silence DeprecationWarnings about os.popen3 in Python 2.6. It's
       
    16 # still sensible for us to use it, since subprocess didn't exist in 2.3.
       
    17 warnings.filterwarnings('ignore', category=DeprecationWarning, message=r'os\.popen3')
       
    18 
       
    19 pythonize_re = re.compile(r'\n\s*//')
       
    20 
       
    21 def handle_extensions(extensions=('html',)):
       
    22     """
       
    23     organizes multiple extensions that are separated with commas or passed by
       
    24     using --extension/-e multiple times.
       
    25 
       
    26     for example: running 'django-admin makemessages -e js,txt -e xhtml -a'
       
    27     would result in a extension list: ['.js', '.txt', '.xhtml']
       
    28 
       
    29     >>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
       
    30     ['.html', '.js']
       
    31     >>> handle_extensions(['.html, txt,.tpl'])
       
    32     ['.html', '.tpl', '.txt']
       
    33     """
       
    34     ext_list = []
       
    35     for ext in extensions:
       
    36         ext_list.extend(ext.replace(' ','').split(','))
       
    37     for i, ext in enumerate(ext_list):
       
    38         if not ext.startswith('.'):
       
    39             ext_list[i] = '.%s' % ext_list[i]
       
    40 
       
    41     # we don't want *.py files here because of the way non-*.py files
       
    42     # are handled in make_messages() (they are copied to file.ext.py files to
       
    43     # trick xgettext to parse them as Python files)
       
    44     return set([x for x in ext_list if x != '.py'])
       
    45 
       
    46 def make_messages(locale=None, domain='django', verbosity='1', all=False, extensions=None):
       
    47     """
       
    48     Uses the locale directory from the Django SVN tree or an application/
       
    49     project to process all
       
    50     """
       
    51     # Need to ensure that the i18n framework is enabled
       
    52     from django.conf import settings
       
    53     if settings.configured:
       
    54         settings.USE_I18N = True
       
    55     else:
       
    56         settings.configure(USE_I18N = True)
       
    57 
       
    58     from django.utils.translation import templatize
       
    59 
       
    60     if os.path.isdir(os.path.join('conf', 'locale')):
       
    61         localedir = os.path.abspath(os.path.join('conf', 'locale'))
       
    62     elif os.path.isdir('locale'):
       
    63         localedir = os.path.abspath('locale')
       
    64     else:
       
    65         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.")
       
    66 
       
    67     if domain not in ('django', 'djangojs'):
       
    68         raise CommandError("currently makemessages only supports domains 'django' and 'djangojs'")
       
    69 
       
    70     if (locale is None and not all) or domain is None:
       
    71         # backwards compatible error message
       
    72         if not sys.argv[0].endswith("make-messages.py"):
       
    73             message = "Type '%s help %s' for usage.\n" % (os.path.basename(sys.argv[0]), sys.argv[1])
       
    74         else:
       
    75             message = "usage: make-messages.py -l <language>\n   or: make-messages.py -a\n"
       
    76         raise CommandError(message)
       
    77 
       
    78     # xgettext versions prior to 0.15 assumed Python source files were encoded
       
    79     # in iso-8859-1, and produce utf-8 output.  In the case where xgettext is
       
    80     # given utf-8 input (required for Django files with non-ASCII characters),
       
    81     # this results in a utf-8 re-encoding of the original utf-8 that needs to be
       
    82     # undone to restore the original utf-8.  So we check the xgettext version
       
    83     # here once and set a flag to remember if a utf-8 decoding needs to be done
       
    84     # on xgettext's output for Python files.  We default to assuming this isn't
       
    85     # necessary if we run into any trouble determining the version.
       
    86     xgettext_reencodes_utf8 = False
       
    87     (stdin, stdout, stderr) = os.popen3('xgettext --version', 't')
       
    88     match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', stdout.read())
       
    89     if match:
       
    90         xversion = (int(match.group('major')), int(match.group('minor')))
       
    91         if xversion < (0, 15):
       
    92             xgettext_reencodes_utf8 = True
       
    93  
       
    94     languages = []
       
    95     if locale is not None:
       
    96         languages.append(locale)
       
    97     elif all:
       
    98         languages = [el for el in os.listdir(localedir) if not el.startswith('.')]
       
    99 
       
   100     for locale in languages:
       
   101         if verbosity > 0:
       
   102             print "processing language", locale
       
   103         basedir = os.path.join(localedir, locale, 'LC_MESSAGES')
       
   104         if not os.path.isdir(basedir):
       
   105             os.makedirs(basedir)
       
   106 
       
   107         pofile = os.path.join(basedir, '%s.po' % domain)
       
   108         potfile = os.path.join(basedir, '%s.pot' % domain)
       
   109 
       
   110         if os.path.exists(potfile):
       
   111             os.unlink(potfile)
       
   112 
       
   113         all_files = []
       
   114         for (dirpath, dirnames, filenames) in os.walk("."):
       
   115             all_files.extend([(dirpath, f) for f in filenames])
       
   116         all_files.sort()
       
   117         for dirpath, file in all_files:
       
   118             file_base, file_ext = os.path.splitext(file)
       
   119             if domain == 'djangojs' and file_ext == '.js':
       
   120                 if verbosity > 1:
       
   121                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
       
   122                 src = open(os.path.join(dirpath, file), "rU").read()
       
   123                 src = pythonize_re.sub('\n#', src)
       
   124                 thefile = '%s.py' % file
       
   125                 open(os.path.join(dirpath, thefile), "w").write(src)
       
   126                 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))
       
   127                 (stdin, stdout, stderr) = os.popen3(cmd, 't')
       
   128                 msgs = stdout.read()
       
   129                 errors = stderr.read()
       
   130                 if errors:
       
   131                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
       
   132                 old = '#: '+os.path.join(dirpath, thefile)[2:]
       
   133                 new = '#: '+os.path.join(dirpath, file)[2:]
       
   134                 msgs = msgs.replace(old, new)
       
   135                 if os.path.exists(potfile):
       
   136                     # Strip the header
       
   137                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
       
   138                 else:
       
   139                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
       
   140                 if msgs:
       
   141                     open(potfile, 'ab').write(msgs)
       
   142                 os.unlink(os.path.join(dirpath, thefile))
       
   143             elif domain == 'django' and (file_ext == '.py' or file_ext in extensions):
       
   144                 thefile = file
       
   145                 if file_ext in extensions:
       
   146                     src = open(os.path.join(dirpath, file), "rU").read()
       
   147                     thefile = '%s.py' % file
       
   148                     open(os.path.join(dirpath, thefile), "w").write(templatize(src))
       
   149                 if verbosity > 1:
       
   150                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
       
   151                 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"' % (
       
   152                     domain, os.path.join(dirpath, thefile))
       
   153                 (stdin, stdout, stderr) = os.popen3(cmd, 't')
       
   154                 msgs = stdout.read()
       
   155                 errors = stderr.read()
       
   156                 if errors:
       
   157                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
       
   158 
       
   159                 if xgettext_reencodes_utf8:
       
   160                     msgs = msgs.decode('utf-8').encode('iso-8859-1')
       
   161 
       
   162                 if thefile != file:
       
   163                     old = '#: '+os.path.join(dirpath, thefile)[2:]
       
   164                     new = '#: '+os.path.join(dirpath, file)[2:]
       
   165                     msgs = msgs.replace(old, new)
       
   166                 if os.path.exists(potfile):
       
   167                     # Strip the header
       
   168                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
       
   169                 else:
       
   170                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
       
   171                 if msgs:
       
   172                     open(potfile, 'ab').write(msgs)
       
   173                 if thefile != file:
       
   174                     os.unlink(os.path.join(dirpath, thefile))
       
   175 
       
   176         if os.path.exists(potfile):
       
   177             (stdin, stdout, stderr) = os.popen3('msguniq --to-code=utf-8 "%s"' % potfile, 't')
       
   178             msgs = stdout.read()
       
   179             errors = stderr.read()
       
   180             if errors:
       
   181                 raise CommandError("errors happened while running msguniq\n%s" % errors)
       
   182             open(potfile, 'w').write(msgs)
       
   183             if os.path.exists(pofile):
       
   184                 (stdin, stdout, stderr) = os.popen3('msgmerge -q "%s" "%s"' % (pofile, potfile), 't')
       
   185                 msgs = stdout.read()
       
   186                 errors = stderr.read()
       
   187                 if errors:
       
   188                     raise CommandError("errors happened while running msgmerge\n%s" % errors)
       
   189             open(pofile, 'wb').write(msgs)
       
   190             os.unlink(potfile)
       
   191 
       
   192 
       
   193 class Command(BaseCommand):
       
   194     option_list = BaseCommand.option_list + (
       
   195         make_option('--locale', '-l', default=None, dest='locale',
       
   196             help='Creates or updates the message files only for the given locale (e.g. pt_BR).'),
       
   197         make_option('--domain', '-d', default='django', dest='domain',
       
   198             help='The domain of the message files (default: "django").'),
       
   199         make_option('--all', '-a', action='store_true', dest='all',
       
   200             default=False, help='Reexamines all source code and templates for new translation strings and updates all message files for all available languages.'),
       
   201         make_option('--extension', '-e', dest='extensions',
       
   202             help='The file extension(s) to examine (default: ".html", separate multiple extensions with commas, or use -e multiple times)',
       
   203             action='append'),
       
   204     )
       
   205     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."
       
   206 
       
   207     requires_model_validation = False
       
   208     can_import_settings = False
       
   209 
       
   210     def handle(self, *args, **options):
       
   211         if len(args) != 0:
       
   212             raise CommandError("Command doesn't accept any arguments")
       
   213 
       
   214         locale = options.get('locale')
       
   215         domain = options.get('domain')
       
   216         verbosity = int(options.get('verbosity'))
       
   217         process_all = options.get('all')
       
   218         extensions = options.get('extensions') or ['html']
       
   219 
       
   220         if domain == 'djangojs':
       
   221             extensions = []
       
   222         else:
       
   223             extensions = handle_extensions(extensions)
       
   224 
       
   225         if '.js' in extensions:
       
   226             raise CommandError("JavaScript files should be examined by using the special 'djangojs' domain only.")
       
   227 
       
   228         make_messages(locale, domain, verbosity, process_all, extensions)