2010-02-16 20:15:04 +08:00
import fnmatch
import glob
2008-07-06 14:39:44 +08:00
import os
2010-02-16 20:15:04 +08:00
import re
2008-07-06 14:39:44 +08:00
import sys
from itertools import dropwhile
from optparse import make_option
2010-01-26 23:01:58 +08:00
from subprocess import PIPE , Popen
2008-10-05 19:39:58 +08:00
2012-02-05 02:27:24 +08:00
import django
2011-01-22 05:00:39 +08:00
from django . core . management . base import CommandError , NoArgsCommand
2010-02-16 20:12:13 +08:00
from django . utils . text import get_text_list
2011-06-08 00:11:25 +08:00
from django . utils . jslex import prepare_js_for_gettext
2008-07-06 14:39:44 +08:00
2010-02-16 20:15:41 +08:00
plural_forms_re = re . compile ( r ' ^(?P<value> " Plural-Forms.+? \\ n " ) \ s*$ ' , re . MULTILINE | re . DOTALL )
2008-07-06 14:39:44 +08:00
2011-12-23 06:38:02 +08:00
def handle_extensions ( extensions = ( ' html ' , ) , ignored = ( ' py ' , ) ) :
2008-08-09 00:41:55 +08:00
"""
2011-09-22 01:25:13 +08:00
Organizes multiple extensions that are separated with commas or passed by
using - - extension / - e multiple times . Note that the . py extension is ignored
here because of the way non - * . py files are handled in make_messages ( ) ( they
are copied to file . ext . py files to trick xgettext to parse them as Python
files ) .
2008-08-09 00:41:55 +08:00
2011-09-22 01:25:13 +08:00
For example : running ' django-admin makemessages -e js,txt -e xhtml -a '
would result in an extension list : [ ' .js ' , ' .txt ' , ' .xhtml ' ]
2008-08-09 00:41:55 +08:00
>> > handle_extensions ( [ ' .html ' , ' html,js,py,py,py,.py ' , ' py,.py ' ] )
2011-09-22 01:25:13 +08:00
set ( [ ' .html ' , ' .js ' ] )
2008-08-09 00:41:55 +08:00
>> > handle_extensions ( [ ' .html, txt,.tpl ' ] )
2011-09-22 01:25:13 +08:00
set ( [ ' .html ' , ' .tpl ' , ' .txt ' ] )
2008-08-09 00:41:55 +08:00
"""
ext_list = [ ]
for ext in extensions :
2011-12-23 06:38:02 +08:00
ext_list . extend ( ext . replace ( ' ' , ' ' ) . split ( ' , ' ) )
2008-08-09 00:41:55 +08:00
for i , ext in enumerate ( ext_list ) :
if not ext . startswith ( ' . ' ) :
ext_list [ i ] = ' . %s ' % ext_list [ i ]
2011-12-23 06:38:02 +08:00
return set ( [ x for x in ext_list if x . strip ( ' . ' ) not in ignored ] )
2008-08-09 00:41:55 +08:00
2010-02-05 08:44:56 +08:00
def _popen ( cmd ) :
"""
Friendly wrapper around Popen for Windows
"""
p = Popen ( cmd , shell = True , stdout = PIPE , stderr = PIPE , close_fds = os . name != ' nt ' , universal_newlines = True )
return p . communicate ( )
2012-02-05 02:27:24 +08:00
def walk ( root , topdown = True , onerror = None , followlinks = False ,
ignore_patterns = [ ] , verbosity = 0 , stdout = sys . stdout ) :
2010-02-16 20:14:27 +08:00
"""
A version of os . walk that can follow symlinks for Python < 2.6
"""
2012-02-05 02:27:24 +08:00
dir_suffix = ' %s * ' % os . sep
norm_patterns = map ( lambda p : p . endswith ( dir_suffix )
and p [ : - len ( dir_suffix ) ] or p , ignore_patterns )
2010-02-16 20:14:27 +08:00
for dirpath , dirnames , filenames in os . walk ( root , topdown , onerror ) :
2012-02-05 02:27:24 +08:00
remove_dirs = [ ]
for dirname in dirnames :
if is_ignored ( os . path . normpath ( os . path . join ( dirpath , dirname ) ) , norm_patterns ) :
remove_dirs . append ( dirname )
for dirname in remove_dirs :
dirnames . remove ( dirname )
if verbosity > 1 :
stdout . write ( ' ignoring directory %s \n ' % dirname )
2010-02-16 20:14:27 +08:00
yield ( dirpath , dirnames , filenames )
if followlinks :
for d in dirnames :
p = os . path . join ( dirpath , d )
if os . path . islink ( p ) :
for link_dirpath , link_dirnames , link_filenames in walk ( p ) :
yield ( link_dirpath , link_dirnames , link_filenames )
2010-02-16 20:15:04 +08:00
def is_ignored ( path , ignore_patterns ) :
"""
Helper function to check if the given path should be ignored or not .
"""
for pattern in ignore_patterns :
if fnmatch . fnmatchcase ( path , pattern ) :
return True
return False
2012-02-05 02:27:24 +08:00
def find_files ( root , ignore_patterns , verbosity , stdout = sys . stdout , symlinks = False ) :
2010-02-16 20:14:27 +08:00
"""
Helper function to get all files in the given root .
"""
all_files = [ ]
2012-02-05 02:27:24 +08:00
for ( dirpath , dirnames , filenames ) in walk ( " . " , followlinks = symlinks ,
ignore_patterns = ignore_patterns , verbosity = verbosity , stdout = stdout ) :
for filename in filenames :
norm_filepath = os . path . normpath ( os . path . join ( dirpath , filename ) )
2010-02-16 20:15:04 +08:00
if is_ignored ( norm_filepath , ignore_patterns ) :
if verbosity > 1 :
2012-02-05 04:02:46 +08:00
stdout . write ( ' ignoring file %s in %s \n ' % ( filename , dirpath ) )
2010-02-16 20:15:04 +08:00
else :
2012-02-05 02:27:24 +08:00
all_files . extend ( [ ( dirpath , filename ) ] )
2010-02-16 20:14:27 +08:00
all_files . sort ( )
return all_files
2012-02-05 02:27:24 +08:00
def copy_plural_forms ( msgs , locale , domain , verbosity , stdout = sys . stdout ) :
2010-02-16 20:15:41 +08:00
"""
Copies plural forms header contents from a Django catalog of locale to
the msgs string , inserting it at the right place . msgs should be the
contents of a newly created . po file .
"""
django_dir = os . path . normpath ( os . path . join ( os . path . dirname ( django . __file__ ) ) )
if domain == ' djangojs ' :
domains = ( ' djangojs ' , ' django ' )
else :
domains = ( ' django ' , )
for domain in domains :
django_po = os . path . join ( django_dir , ' conf ' , ' locale ' , locale , ' LC_MESSAGES ' , ' %s .po ' % domain )
if os . path . exists ( django_po ) :
m = plural_forms_re . search ( open ( django_po , ' rU ' ) . read ( ) )
if m :
if verbosity > 1 :
2012-02-05 02:27:24 +08:00
stdout . write ( " copying plural forms: %s \n " % m . group ( ' value ' ) )
2010-02-16 20:15:41 +08:00
lines = [ ]
seen = False
for line in msgs . split ( ' \n ' ) :
if not line and not seen :
line = ' %s \n ' % m . group ( ' value ' )
seen = True
lines . append ( line )
msgs = ' \n ' . join ( lines )
break
return msgs
2011-12-23 19:24:35 +08:00
def write_pot_file ( potfile , msgs , file , work_file , is_templatized ) :
"""
Write the : param potfile : POT file with the : param msgs : contents ,
previously making sure its format is valid .
"""
if is_templatized :
old = ' #: ' + work_file [ 2 : ]
new = ' #: ' + file [ 2 : ]
msgs = msgs . replace ( old , new )
if os . path . exists ( potfile ) :
# Strip the header
msgs = ' \n ' . join ( dropwhile ( len , msgs . split ( ' \n ' ) ) )
else :
msgs = msgs . replace ( ' charset=CHARSET ' , ' charset=UTF-8 ' )
f = open ( potfile , ' ab ' )
try :
f . write ( msgs )
finally :
f . close ( )
2012-02-05 02:27:24 +08:00
def process_file ( file , dirpath , potfile , domain , verbosity ,
extensions , wrap , location , stdout = sys . stdout ) :
2011-12-23 19:24:35 +08:00
"""
Extract translatable literals from : param file : for : param domain :
creating or updating the : param potfile : POT file .
Uses the xgettext GNU gettext utility .
"""
from django . utils . translation import templatize
if verbosity > 1 :
2012-02-05 02:27:24 +08:00
stdout . write ( ' processing file %s in %s \n ' % ( file , dirpath ) )
2011-12-23 19:24:35 +08:00
_ , file_ext = os . path . splitext ( file )
if domain == ' djangojs ' and file_ext in extensions :
is_templatized = True
orig_file = os . path . join ( dirpath , file )
src_data = open ( orig_file ) . read ( )
src_data = prepare_js_for_gettext ( src_data )
thefile = ' %s .c ' % file
work_file = os . path . join ( dirpath , thefile )
f = open ( work_file , " w " )
try :
f . write ( src_data )
finally :
f . close ( )
cmd = (
' xgettext -d %s -L C %s %s --keyword=gettext_noop '
' --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 '
' --keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 '
2012-02-05 02:27:24 +08:00
' --from-code UTF-8 --add-comments=Translators -o - " %s " ' %
( domain , wrap , location , work_file ) )
2011-12-23 19:24:35 +08:00
elif domain == ' django ' and ( file_ext == ' .py ' or file_ext in extensions ) :
thefile = file
orig_file = os . path . join ( dirpath , file )
is_templatized = file_ext in extensions
if is_templatized :
src_data = open ( orig_file , " rU " ) . read ( )
thefile = ' %s .py ' % file
content = templatize ( src_data , orig_file [ 2 : ] )
f = open ( os . path . join ( dirpath , thefile ) , " w " )
try :
f . write ( content )
finally :
f . close ( )
work_file = os . path . join ( dirpath , thefile )
cmd = (
' xgettext -d %s -L Python %s %s --keyword=gettext_noop '
' --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 '
' --keyword=ugettext_noop --keyword=ugettext_lazy '
' --keyword=ungettext_lazy:1,2 --keyword=pgettext:1c,2 '
' --keyword=npgettext:1c,2,3 --keyword=pgettext_lazy:1c,2 '
' --keyword=npgettext_lazy:1c,2,3 --from-code UTF-8 '
2012-02-05 02:27:24 +08:00
' --add-comments=Translators -o - " %s " ' %
( domain , wrap , location , work_file ) )
2011-12-23 19:24:35 +08:00
else :
return
msgs , errors = _popen ( cmd )
if errors :
if is_templatized :
os . unlink ( work_file )
if os . path . exists ( potfile ) :
os . unlink ( potfile )
raise CommandError (
" errors happened while running xgettext on %s \n %s " %
( file , errors ) )
if msgs :
write_pot_file ( potfile , msgs , orig_file , work_file , is_templatized )
if is_templatized :
os . unlink ( work_file )
2012-02-05 02:27:24 +08:00
def write_po_file ( pofile , potfile , domain , locale , verbosity , stdout ,
copy_pforms , wrap , location , no_obsolete ) :
2011-12-23 19:24:35 +08:00
"""
Creates of updates the : param pofile : PO file for : param domain : and : param
locale : . Uses contents of the existing : param potfile : .
Uses mguniq , msgmerge , and msgattrib GNU gettext utilities .
"""
msgs , errors = _popen ( ' msguniq %s %s --to-code=utf-8 " %s " ' %
( wrap , location , potfile ) )
if errors :
os . unlink ( potfile )
raise CommandError ( " errors happened while running msguniq \n %s " % errors )
if os . path . exists ( pofile ) :
f = open ( potfile , ' w ' )
try :
f . write ( msgs )
finally :
f . close ( )
msgs , errors = _popen ( ' msgmerge %s %s -q " %s " " %s " ' %
( wrap , location , pofile , potfile ) )
if errors :
os . unlink ( potfile )
raise CommandError (
" errors happened while running msgmerge \n %s " % errors )
elif copy_pforms :
2012-02-05 02:27:24 +08:00
msgs = copy_plural_forms ( msgs , locale , domain , verbosity , stdout )
2011-12-23 19:24:35 +08:00
msgs = msgs . replace (
" #. #-#-#-#-# %s .pot (PACKAGE VERSION) #-#-#-#-# \n " % domain , " " )
f = open ( pofile , ' wb ' )
try :
f . write ( msgs )
finally :
f . close ( )
os . unlink ( potfile )
if no_obsolete :
msgs , errors = _popen ( ' msgattrib %s %s -o " %s " --no-obsolete " %s " ' %
( wrap , location , pofile , pofile ) )
if errors :
raise CommandError (
" errors happened while running msgattrib \n %s " % errors )
2010-02-16 20:15:41 +08:00
2011-12-23 19:24:35 +08:00
def make_messages ( locale = None , domain = ' django ' , verbosity = 1 , all = False ,
extensions = None , symlinks = False , ignore_patterns = None , no_wrap = False ,
2012-02-05 02:27:24 +08:00
no_location = False , no_obsolete = False , stdout = sys . stdout ) :
2008-07-06 14:39:44 +08:00
"""
2011-12-23 19:24:35 +08:00
Uses the ` ` locale / ` ` directory from the Django SVN tree or an
application / project to process all files with translatable literals for
the : param domain : domain and : param locale : locale .
2008-07-06 14:39:44 +08:00
"""
# Need to ensure that the i18n framework is enabled
from django . conf import settings
2008-07-06 20:55:24 +08:00
if settings . configured :
settings . USE_I18N = True
else :
settings . configure ( USE_I18N = True )
2011-12-23 19:24:35 +08:00
if ignore_patterns is None :
ignore_patterns = [ ]
2008-07-06 14:39:44 +08:00
2010-02-16 20:15:41 +08:00
invoked_for_django = False
2008-07-06 14:39:44 +08:00
if os . path . isdir ( os . path . join ( ' conf ' , ' locale ' ) ) :
localedir = os . path . abspath ( os . path . join ( ' conf ' , ' locale ' ) )
2010-02-16 20:15:41 +08:00
invoked_for_django = True
2011-01-22 01:30:35 +08:00
# Ignoring all contrib apps
ignore_patterns + = [ ' contrib/* ' ]
2008-07-06 14:39:44 +08:00
elif os . path . isdir ( ' locale ' ) :
localedir = os . path . abspath ( ' locale ' )
else :
2011-12-23 19:24:35 +08:00
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. " )
2008-07-06 20:55:24 +08:00
2008-07-06 14:39:44 +08:00
if domain not in ( ' django ' , ' djangojs ' ) :
raise CommandError ( " currently makemessages only supports domains ' django ' and ' djangojs ' " )
if ( locale is None and not all ) or domain is None :
2011-01-25 09:39:39 +08:00
message = " Type ' %s help %s ' for usage information. " % ( os . path . basename ( sys . argv [ 0 ] ) , sys . argv [ 1 ] )
2008-07-06 14:39:44 +08:00
raise CommandError ( message )
2010-01-26 23:01:58 +08:00
# We require gettext version 0.15 or newer.
2010-02-05 08:44:56 +08:00
output = _popen ( ' xgettext --version ' ) [ 0 ]
match = re . search ( r ' (?P<major> \ d+) \ .(?P<minor> \ d+) ' , output )
2008-10-06 09:36:35 +08:00
if match :
xversion = ( int ( match . group ( ' major ' ) ) , int ( match . group ( ' minor ' ) ) )
if xversion < ( 0 , 15 ) :
2011-12-23 19:24:35 +08:00
raise CommandError ( " Django internationalization requires GNU "
" gettext 0.15 or newer. You are using version %s , please "
" upgrade your gettext toolset. " % match . group ( ) )
2010-01-26 23:01:58 +08:00
2011-12-23 19:24:35 +08:00
locales = [ ]
2008-07-06 14:39:44 +08:00
if locale is not None :
2011-12-23 19:24:35 +08:00
locales . append ( locale )
2008-07-06 14:39:44 +08:00
elif all :
2010-01-26 23:01:58 +08:00
locale_dirs = filter ( os . path . isdir , glob . glob ( ' %s /* ' % localedir ) )
2011-12-23 19:24:35 +08:00
locales = [ os . path . basename ( l ) for l in locale_dirs ]
2010-01-26 23:01:58 +08:00
2011-12-23 19:24:35 +08:00
wrap = ' --no-wrap ' if no_wrap else ' '
location = ' --no-location ' if no_location else ' '
2010-11-04 20:08:37 +08:00
2011-12-23 19:24:35 +08:00
for locale in locales :
2008-07-06 14:39:44 +08:00
if verbosity > 0 :
2012-02-05 02:27:24 +08:00
stdout . write ( " processing language %s " % locale )
2008-07-06 14:39:44 +08:00
basedir = os . path . join ( localedir , locale , ' LC_MESSAGES ' )
if not os . path . isdir ( basedir ) :
os . makedirs ( basedir )
pofile = os . path . join ( basedir , ' %s .po ' % domain )
potfile = os . path . join ( basedir , ' %s .pot ' % domain )
if os . path . exists ( potfile ) :
os . unlink ( potfile )
2011-12-23 19:24:35 +08:00
for dirpath , file in find_files ( " . " , ignore_patterns , verbosity ,
2012-02-05 02:27:24 +08:00
stdout , symlinks = symlinks ) :
2011-12-23 19:24:35 +08:00
process_file ( file , dirpath , potfile , domain , verbosity , extensions ,
2012-02-05 02:27:24 +08:00
wrap , location , stdout )
2008-07-06 14:39:44 +08:00
if os . path . exists ( potfile ) :
2012-02-05 02:27:24 +08:00
write_po_file ( pofile , potfile , domain , locale , verbosity , stdout ,
2011-12-23 19:24:35 +08:00
not invoked_for_django , wrap , location , no_obsolete )
2008-07-06 14:39:44 +08:00
2011-01-22 05:00:39 +08:00
class Command ( NoArgsCommand ) :
option_list = NoArgsCommand . option_list + (
2008-07-06 14:39:44 +08:00
make_option ( ' --locale ' , ' -l ' , default = None , dest = ' locale ' ,
2011-01-25 09:39:39 +08:00
help = ' Creates or updates the message files for the given locale (e.g. pt_BR). ' ) ,
2008-07-06 14:39:44 +08:00
make_option ( ' --domain ' , ' -d ' , default = ' django ' , dest = ' domain ' ,
help = ' The domain of the message files (default: " django " ). ' ) ,
make_option ( ' --all ' , ' -a ' , action = ' store_true ' , dest = ' all ' ,
2011-01-25 09:39:39 +08:00
default = False , help = ' Updates the message files for all existing locales. ' ) ,
2008-08-09 00:41:55 +08:00
make_option ( ' --extension ' , ' -e ' , dest = ' extensions ' ,
2011-09-22 01:25:13 +08:00
help = ' The file extension(s) to examine (default: " html,txt " , or " js " if the domain is " djangojs " ). Separate multiple extensions with commas, or use -e multiple times. ' ,
2008-08-09 00:41:55 +08:00
action = ' append ' ) ,
2010-02-16 20:15:04 +08:00
make_option ( ' --symlinks ' , ' -s ' , action = ' store_true ' , dest = ' symlinks ' ,
default = False , help = ' Follows symlinks to directories when examining source code and templates for translation strings. ' ) ,
make_option ( ' --ignore ' , ' -i ' , action = ' append ' , dest = ' ignore_patterns ' ,
default = [ ] , metavar = ' PATTERN ' , help = ' Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more. ' ) ,
make_option ( ' --no-default-ignore ' , action = ' store_false ' , dest = ' use_default_ignore_patterns ' ,
default = True , help = " Don ' t ignore the common glob-style patterns ' CVS ' , ' .* ' and ' *~ ' . " ) ,
2010-11-04 20:08:37 +08:00
make_option ( ' --no-wrap ' , action = ' store_true ' , dest = ' no_wrap ' ,
default = False , help = " Don ' t break long message lines into several lines " ) ,
2011-11-11 21:07:14 +08:00
make_option ( ' --no-location ' , action = ' store_true ' , dest = ' no_location ' ,
default = False , help = " Don ' t write ' #: filename:line ' lines " ) ,
2011-01-22 01:30:35 +08:00
make_option ( ' --no-obsolete ' , action = ' store_true ' , dest = ' no_obsolete ' ,
default = False , help = " Remove obsolete message strings " ) ,
2008-07-06 14:39:44 +08:00
)
2012-02-05 02:27:24 +08:00
help = ( " Runs over the entire source tree of the current directory and "
2011-01-25 09:39:39 +08:00
" 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 projects and "
" applications) directory. \n \n You must run this command with one of either the "
" --locale or --all options. " )
2008-07-06 14:39:44 +08:00
requires_model_validation = False
can_import_settings = False
2011-01-22 05:00:39 +08:00
def handle_noargs ( self , * args , * * options ) :
2008-07-06 14:39:44 +08:00
locale = options . get ( ' locale ' )
domain = options . get ( ' domain ' )
verbosity = int ( options . get ( ' verbosity ' ) )
process_all = options . get ( ' all ' )
2010-02-16 20:12:13 +08:00
extensions = options . get ( ' extensions ' )
2010-02-16 20:14:27 +08:00
symlinks = options . get ( ' symlinks ' )
2010-02-16 20:15:04 +08:00
ignore_patterns = options . get ( ' ignore_patterns ' )
if options . get ( ' use_default_ignore_patterns ' ) :
ignore_patterns + = [ ' CVS ' , ' .* ' , ' *~ ' ]
ignore_patterns = list ( set ( ignore_patterns ) )
2010-11-04 20:08:37 +08:00
no_wrap = options . get ( ' no_wrap ' )
2011-11-11 21:07:14 +08:00
no_location = options . get ( ' no_location ' )
2011-01-22 01:30:35 +08:00
no_obsolete = options . get ( ' no_obsolete ' )
2008-08-09 00:41:55 +08:00
if domain == ' djangojs ' :
2010-02-16 20:12:13 +08:00
extensions = handle_extensions ( extensions or [ ' js ' ] )
2008-08-09 00:41:55 +08:00
else :
2011-06-19 19:24:25 +08:00
extensions = handle_extensions ( extensions or [ ' html ' , ' txt ' ] )
2008-08-09 00:41:55 +08:00
2010-02-16 20:12:13 +08:00
if verbosity > 1 :
2012-02-05 02:27:24 +08:00
self . stdout . write ( ' examining files with the extensions: %s \n '
2010-11-04 20:08:37 +08:00
% get_text_list ( list ( extensions ) , ' and ' ) )
2008-07-06 14:39:44 +08:00
2012-02-05 02:27:24 +08:00
make_messages ( locale , domain , verbosity , process_all , extensions ,
symlinks , ignore_patterns , no_wrap , no_location , no_obsolete , self . stdout )