web/lib/django/core/management/commands/makemessages.py
changeset 29 cc9b7e14412b
parent 0 0d40e90630ef
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
       
     1 import fnmatch
       
     2 import glob
       
     3 import os
     1 import re
     4 import re
     2 import os
       
     3 import sys
     5 import sys
     4 import glob
       
     5 import warnings
       
     6 from itertools import dropwhile
     6 from itertools import dropwhile
     7 from optparse import make_option
     7 from optparse import make_option
       
     8 from subprocess import PIPE, Popen
     8 
     9 
     9 from django.core.management.base import CommandError, BaseCommand
    10 from django.core.management.base import CommandError, BaseCommand
    10 
    11 from django.utils.text import get_text_list
    11 try:
    12 
    12     set
    13 pythonize_re = re.compile(r'(?:^|\n)\s*//')
    13 except NameError:
    14 plural_forms_re = re.compile(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL)
    14     from sets import Set as set     # For Python 2.3
       
    15 
       
    16 # Intentionally silence DeprecationWarnings about os.popen3 in Python 2.6. It's
       
    17 # still sensible for us to use it, since subprocess didn't exist in 2.3.
       
    18 warnings.filterwarnings('ignore', category=DeprecationWarning, message=r'os\.popen3')
       
    19 
       
    20 pythonize_re = re.compile(r'\n\s*//')
       
    21 
    15 
    22 def handle_extensions(extensions=('html',)):
    16 def handle_extensions(extensions=('html',)):
    23     """
    17     """
    24     organizes multiple extensions that are separated with commas or passed by
    18     organizes multiple extensions that are separated with commas or passed by
    25     using --extension/-e multiple times.
    19     using --extension/-e multiple times.
    42     # we don't want *.py files here because of the way non-*.py files
    36     # we don't want *.py files here because of the way non-*.py files
    43     # are handled in make_messages() (they are copied to file.ext.py files to
    37     # are handled in make_messages() (they are copied to file.ext.py files to
    44     # trick xgettext to parse them as Python files)
    38     # trick xgettext to parse them as Python files)
    45     return set([x for x in ext_list if x != '.py'])
    39     return set([x for x in ext_list if x != '.py'])
    46 
    40 
    47 def make_messages(locale=None, domain='django', verbosity='1', all=False, extensions=None):
    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=[]):
    48     """
   119     """
    49     Uses the locale directory from the Django SVN tree or an application/
   120     Uses the locale directory from the Django SVN tree or an application/
    50     project to process all
   121     project to process all
    51     """
   122     """
    52     # Need to ensure that the i18n framework is enabled
   123     # Need to ensure that the i18n framework is enabled
    56     else:
   127     else:
    57         settings.configure(USE_I18N = True)
   128         settings.configure(USE_I18N = True)
    58 
   129 
    59     from django.utils.translation import templatize
   130     from django.utils.translation import templatize
    60 
   131 
       
   132     invoked_for_django = False
    61     if os.path.isdir(os.path.join('conf', 'locale')):
   133     if os.path.isdir(os.path.join('conf', 'locale')):
    62         localedir = os.path.abspath(os.path.join('conf', 'locale'))
   134         localedir = os.path.abspath(os.path.join('conf', 'locale'))
       
   135         invoked_for_django = True
    63     elif os.path.isdir('locale'):
   136     elif os.path.isdir('locale'):
    64         localedir = os.path.abspath('locale')
   137         localedir = os.path.abspath('locale')
    65     else:
   138     else:
    66         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.")
   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.")
    67 
   140 
    74             message = "Type '%s help %s' for usage.\n" % (os.path.basename(sys.argv[0]), sys.argv[1])
   147             message = "Type '%s help %s' for usage.\n" % (os.path.basename(sys.argv[0]), sys.argv[1])
    75         else:
   148         else:
    76             message = "usage: make-messages.py -l <language>\n   or: make-messages.py -a\n"
   149             message = "usage: make-messages.py -l <language>\n   or: make-messages.py -a\n"
    77         raise CommandError(message)
   150         raise CommandError(message)
    78 
   151 
    79     # xgettext versions prior to 0.15 assumed Python source files were encoded
   152     # We require gettext version 0.15 or newer.
    80     # in iso-8859-1, and produce utf-8 output.  In the case where xgettext is
   153     output = _popen('xgettext --version')[0]
    81     # given utf-8 input (required for Django files with non-ASCII characters),
   154     match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output)
    82     # this results in a utf-8 re-encoding of the original utf-8 that needs to be
       
    83     # undone to restore the original utf-8.  So we check the xgettext version
       
    84     # here once and set a flag to remember if a utf-8 decoding needs to be done
       
    85     # on xgettext's output for Python files.  We default to assuming this isn't
       
    86     # necessary if we run into any trouble determining the version.
       
    87     xgettext_reencodes_utf8 = False
       
    88     (stdin, stdout, stderr) = os.popen3('xgettext --version', 't')
       
    89     match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', stdout.read())
       
    90     if match:
   155     if match:
    91         xversion = (int(match.group('major')), int(match.group('minor')))
   156         xversion = (int(match.group('major')), int(match.group('minor')))
    92         if xversion < (0, 15):
   157         if xversion < (0, 15):
    93             xgettext_reencodes_utf8 = True
   158             raise CommandError("Django internationalization requires GNU gettext 0.15 or newer. You are using version %s, please upgrade your gettext toolset." % match.group())
    94  
   159 
    95     languages = []
   160     languages = []
    96     if locale is not None:
   161     if locale is not None:
    97         languages.append(locale)
   162         languages.append(locale)
    98     elif all:
   163     elif all:
    99         locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) 
   164         locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir))
   100         languages = [os.path.basename(l) for l in locale_dirs]
   165         languages = [os.path.basename(l) for l in locale_dirs]
   101     
   166 
   102     for locale in languages:
   167     for locale in languages:
   103         if verbosity > 0:
   168         if verbosity > 0:
   104             print "processing language", locale
   169             print "processing language", locale
   105         basedir = os.path.join(localedir, locale, 'LC_MESSAGES')
   170         basedir = os.path.join(localedir, locale, 'LC_MESSAGES')
   106         if not os.path.isdir(basedir):
   171         if not os.path.isdir(basedir):
   110         potfile = os.path.join(basedir, '%s.pot' % domain)
   175         potfile = os.path.join(basedir, '%s.pot' % domain)
   111 
   176 
   112         if os.path.exists(potfile):
   177         if os.path.exists(potfile):
   113             os.unlink(potfile)
   178             os.unlink(potfile)
   114 
   179 
   115         all_files = []
   180         for dirpath, file in find_files(".", ignore_patterns, verbosity, symlinks=symlinks):
   116         for (dirpath, dirnames, filenames) in os.walk("."):
       
   117             all_files.extend([(dirpath, f) for f in filenames])
       
   118         all_files.sort()
       
   119         for dirpath, file in all_files:
       
   120             file_base, file_ext = os.path.splitext(file)
   181             file_base, file_ext = os.path.splitext(file)
   121             if domain == 'djangojs' and file_ext == '.js':
   182             if domain == 'djangojs' and file_ext in extensions:
   122                 if verbosity > 1:
   183                 if verbosity > 1:
   123                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
   184                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
   124                 src = open(os.path.join(dirpath, file), "rU").read()
   185                 src = open(os.path.join(dirpath, file), "rU").read()
   125                 src = pythonize_re.sub('\n#', src)
   186                 src = pythonize_re.sub('\n#', src)
   126                 thefile = '%s.py' % file
   187                 thefile = '%s.py' % file
   127                 open(os.path.join(dirpath, thefile), "w").write(src)
   188                 f = open(os.path.join(dirpath, thefile), "w")
       
   189                 try:
       
   190                     f.write(src)
       
   191                 finally:
       
   192                     f.close()
   128                 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))
   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))
   129                 (stdin, stdout, stderr) = os.popen3(cmd, 't')
   194                 msgs, errors = _popen(cmd)
   130                 msgs = stdout.read()
       
   131                 errors = stderr.read()
       
   132                 if errors:
   195                 if errors:
   133                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
   196                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
   134                 old = '#: '+os.path.join(dirpath, thefile)[2:]
   197                 old = '#: '+os.path.join(dirpath, thefile)[2:]
   135                 new = '#: '+os.path.join(dirpath, file)[2:]
   198                 new = '#: '+os.path.join(dirpath, file)[2:]
   136                 msgs = msgs.replace(old, new)
   199                 msgs = msgs.replace(old, new)
   138                     # Strip the header
   201                     # Strip the header
   139                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
   202                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
   140                 else:
   203                 else:
   141                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
   204                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
   142                 if msgs:
   205                 if msgs:
   143                     open(potfile, 'ab').write(msgs)
   206                     f = open(potfile, 'ab')
       
   207                     try:
       
   208                         f.write(msgs)
       
   209                     finally:
       
   210                         f.close()
   144                 os.unlink(os.path.join(dirpath, thefile))
   211                 os.unlink(os.path.join(dirpath, thefile))
   145             elif domain == 'django' and (file_ext == '.py' or file_ext in extensions):
   212             elif domain == 'django' and (file_ext == '.py' or file_ext in extensions):
   146                 thefile = file
   213                 thefile = file
   147                 if file_ext in extensions:
   214                 if file_ext in extensions:
   148                     src = open(os.path.join(dirpath, file), "rU").read()
   215                     src = open(os.path.join(dirpath, file), "rU").read()
   149                     thefile = '%s.py' % file
   216                     thefile = '%s.py' % file
   150                     try:
   217                     try:
   151                         open(os.path.join(dirpath, thefile), "w").write(templatize(src))
   218                         f = open(os.path.join(dirpath, thefile), "w")
       
   219                         try:
       
   220                             f.write(templatize(src))
       
   221                         finally:
       
   222                             f.close()
   152                     except SyntaxError, msg:
   223                     except SyntaxError, msg:
   153                         msg = "%s (file: %s)" % (msg, os.path.join(dirpath, file))
   224                         msg = "%s (file: %s)" % (msg, os.path.join(dirpath, file))
   154                         raise SyntaxError(msg)
   225                         raise SyntaxError(msg)
   155                 if verbosity > 1:
   226                 if verbosity > 1:
   156                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
   227                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
   157                 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"' % (
   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"' % (
   158                     domain, os.path.join(dirpath, thefile))
   229                     domain, os.path.join(dirpath, thefile))
   159                 (stdin, stdout, stderr) = os.popen3(cmd, 't')
   230                 msgs, errors = _popen(cmd)
   160                 msgs = stdout.read()
       
   161                 errors = stderr.read()
       
   162                 if errors:
   231                 if errors:
   163                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
   232                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
   164 
       
   165                 if xgettext_reencodes_utf8:
       
   166                     msgs = msgs.decode('utf-8').encode('iso-8859-1')
       
   167 
   233 
   168                 if thefile != file:
   234                 if thefile != file:
   169                     old = '#: '+os.path.join(dirpath, thefile)[2:]
   235                     old = '#: '+os.path.join(dirpath, thefile)[2:]
   170                     new = '#: '+os.path.join(dirpath, file)[2:]
   236                     new = '#: '+os.path.join(dirpath, file)[2:]
   171                     msgs = msgs.replace(old, new)
   237                     msgs = msgs.replace(old, new)
   173                     # Strip the header
   239                     # Strip the header
   174                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
   240                     msgs = '\n'.join(dropwhile(len, msgs.split('\n')))
   175                 else:
   241                 else:
   176                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
   242                     msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8')
   177                 if msgs:
   243                 if msgs:
   178                     open(potfile, 'ab').write(msgs)
   244                     f = open(potfile, 'ab')
       
   245                     try:
       
   246                         f.write(msgs)
       
   247                     finally:
       
   248                         f.close()
   179                 if thefile != file:
   249                 if thefile != file:
   180                     os.unlink(os.path.join(dirpath, thefile))
   250                     os.unlink(os.path.join(dirpath, thefile))
   181 
   251 
   182         if os.path.exists(potfile):
   252         if os.path.exists(potfile):
   183             (stdin, stdout, stderr) = os.popen3('msguniq --to-code=utf-8 "%s"' % potfile, 't')
   253             msgs, errors = _popen('msguniq --to-code=utf-8 "%s"' % potfile)
   184             msgs = stdout.read()
       
   185             errors = stderr.read()
       
   186             if errors:
   254             if errors:
   187                 raise CommandError("errors happened while running msguniq\n%s" % errors)
   255                 raise CommandError("errors happened while running msguniq\n%s" % errors)
   188             open(potfile, 'w').write(msgs)
   256             f = open(potfile, 'w')
       
   257             try:
       
   258                 f.write(msgs)
       
   259             finally:
       
   260                 f.close()
   189             if os.path.exists(pofile):
   261             if os.path.exists(pofile):
   190                 (stdin, stdout, stderr) = os.popen3('msgmerge -q "%s" "%s"' % (pofile, potfile), 't')
   262                 msgs, errors = _popen('msgmerge -q "%s" "%s"' % (pofile, potfile))
   191                 msgs = stdout.read()
       
   192                 errors = stderr.read()
       
   193                 if errors:
   263                 if errors:
   194                     raise CommandError("errors happened while running msgmerge\n%s" % errors)
   264                     raise CommandError("errors happened while running msgmerge\n%s" % errors)
   195             open(pofile, 'wb').write(msgs)
   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()
   196             os.unlink(potfile)
   272             os.unlink(potfile)
   197 
   273 
   198 
   274 
   199 class Command(BaseCommand):
   275 class Command(BaseCommand):
   200     option_list = BaseCommand.option_list + (
   276     option_list = BaseCommand.option_list + (
   205         make_option('--all', '-a', action='store_true', dest='all',
   281         make_option('--all', '-a', action='store_true', dest='all',
   206             default=False, help='Reexamines all source code and templates for new translation strings and updates all message files for all available languages.'),
   282             default=False, help='Reexamines all source code and templates for new translation strings and updates all message files for all available languages.'),
   207         make_option('--extension', '-e', dest='extensions',
   283         make_option('--extension', '-e', dest='extensions',
   208             help='The file extension(s) to examine (default: ".html", separate multiple extensions with commas, or use -e multiple times)',
   284             help='The file extension(s) to examine (default: ".html", separate multiple extensions with commas, or use -e multiple times)',
   209             action='append'),
   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 '*~'."),
   210     )
   292     )
   211     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."
   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."
   212 
   294 
   213     requires_model_validation = False
   295     requires_model_validation = False
   214     can_import_settings = False
   296     can_import_settings = False
   219 
   301 
   220         locale = options.get('locale')
   302         locale = options.get('locale')
   221         domain = options.get('domain')
   303         domain = options.get('domain')
   222         verbosity = int(options.get('verbosity'))
   304         verbosity = int(options.get('verbosity'))
   223         process_all = options.get('all')
   305         process_all = options.get('all')
   224         extensions = options.get('extensions') or ['html']
   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))
   225 
   312 
   226         if domain == 'djangojs':
   313         if domain == 'djangojs':
   227             extensions = []
   314             extensions = handle_extensions(extensions or ['js'])
   228         else:
   315         else:
   229             extensions = handle_extensions(extensions)
   316             extensions = handle_extensions(extensions or ['html'])
   230 
   317 
   231         if '.js' in extensions:
   318         if verbosity > 1:
   232             raise CommandError("JavaScript files should be examined by using the special 'djangojs' domain only.")
   319             sys.stdout.write('examining files with the extensions: %s\n' % get_text_list(list(extensions), 'and'))
   233 
   320 
   234         make_messages(locale, domain, verbosity, process_all, extensions)
   321         make_messages(locale, domain, verbosity, process_all, extensions, symlinks, ignore_patterns)