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
2013-01-17 20:33:04 +08:00
from django . utils . functional import total_ordering
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 )
2012-07-19 00:34:13 +08:00
STATUS_OK = 0
2008-07-06 14:39:44 +08:00
2008-08-09 00:41:55 +08:00
2013-01-17 20:33:04 +08:00
@total_ordering
class TranslatableFile ( object ) :
2013-01-26 00:58:37 +08:00
def __init__ ( self , dirpath , file_name ) :
2013-01-17 20:33:04 +08:00
self . file = file_name
self . dirpath = dirpath
def __repr__ ( self ) :
return " <TranslatableFile: %s > " % os . sep . join ( [ self . dirpath , self . file ] )
def __eq__ ( self , other ) :
2013-01-26 00:58:37 +08:00
return self . dirpath == other . dirpath and self . file == other . file
2013-01-17 20:33:04 +08:00
def __lt__ ( self , other ) :
2013-01-26 00:58:37 +08:00
if self . dirpath == other . dirpath :
return self . file < other . file
return self . dirpath < other . dirpath
2013-01-17 20:33:04 +08:00
2013-01-26 00:58:37 +08:00
def process ( self , command , potfile , domain , keep_pot = False ) :
2013-01-17 20:33:04 +08:00
"""
2013-01-26 00:58:37 +08:00
Extract translatable literals from self . file for : param domain :
creating or updating the : param potfile : POT file .
2013-01-17 20:33:04 +08:00
Uses the xgettext GNU gettext utility .
"""
from django . utils . translation import templatize
if command . verbosity > 1 :
command . stdout . write ( ' processing file %s in %s \n ' % ( self . file , self . dirpath ) )
_ , file_ext = os . path . splitext ( self . file )
if domain == ' djangojs ' and file_ext in command . extensions :
is_templatized = True
orig_file = os . path . join ( self . dirpath , self . file )
with open ( orig_file ) as fp :
src_data = fp . read ( )
src_data = prepare_js_for_gettext ( src_data )
thefile = ' %s .c ' % self . file
work_file = os . path . join ( self . dirpath , thefile )
with open ( work_file , " w " ) as fp :
fp . write ( src_data )
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 '
' --from-code UTF-8 --add-comments=Translators -o - " %s " ' %
( domain , command . wrap , command . location , work_file ) )
elif domain == ' django ' and ( file_ext == ' .py ' or file_ext in command . extensions ) :
thefile = self . file
orig_file = os . path . join ( self . dirpath , self . file )
is_templatized = file_ext in command . extensions
if is_templatized :
with open ( orig_file , " rU " ) as fp :
src_data = fp . read ( )
thefile = ' %s .py ' % self . file
content = templatize ( src_data , orig_file [ 2 : ] )
with open ( os . path . join ( self . dirpath , thefile ) , " w " ) as fp :
fp . write ( content )
work_file = os . path . join ( self . 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 '
' --add-comments=Translators -o - " %s " ' %
( domain , command . wrap , command . location , work_file ) )
else :
return
msgs , errors , status = _popen ( cmd )
if errors :
if status != STATUS_OK :
if is_templatized :
os . unlink ( work_file )
2013-01-26 00:58:37 +08:00
if not keep_pot and os . path . exists ( potfile ) :
os . unlink ( potfile )
2013-01-17 20:33:04 +08:00
raise CommandError (
" errors happened while running xgettext on %s \n %s " %
( self . file , errors ) )
elif command . verbosity > 0 :
# Print warnings
command . stdout . write ( errors )
if msgs :
if is_templatized :
old = ' #: ' + work_file [ 2 : ]
new = ' #: ' + orig_file [ 2 : ]
msgs = msgs . replace ( old , new )
write_pot_file ( potfile , msgs )
if is_templatized :
os . unlink ( work_file )
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 )
2012-07-19 00:34:13 +08:00
output , errors = p . communicate ( )
return output , errors , p . returncode
2010-02-05 08:44:56 +08:00
2013-01-17 20:33:04 +08:00
def write_pot_file ( potfile , msgs ) :
2011-12-23 19:24:35 +08:00
"""
Write the : param potfile : POT file with the : param msgs : contents ,
previously making sure its format is valid .
"""
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 ' )
2012-08-14 00:22:23 +08:00
with open ( potfile , ' a ' ) as fp :
2012-05-05 20:01:38 +08:00
fp . write ( msgs )
2011-12-23 19:24:35 +08:00
2013-01-17 20:33:04 +08:00
def handle_extensions ( extensions = ( ' html ' , ) , ignored = ( ' py ' , ) ) :
2011-12-23 19:24:35 +08:00
"""
2013-01-17 20:33:04 +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 ) .
2012-07-19 00:34:13 +08:00
2013-01-17 20:33:04 +08:00
For example : running ' django-admin makemessages -e js,txt -e xhtml -a '
would result in an extension list : [ ' .js ' , ' .txt ' , ' .xhtml ' ]
2010-02-16 20:15:41 +08:00
2013-01-17 20:33:04 +08:00
>> > handle_extensions ( [ ' .html ' , ' html,js,py,py,py,.py ' , ' py,.py ' ] )
set ( [ ' .html ' , ' .js ' ] )
>> > handle_extensions ( [ ' .html, txt,.tpl ' ] )
set ( [ ' .html ' , ' .tpl ' , ' .txt ' ] )
2008-07-06 14:39:44 +08:00
"""
2013-01-17 20:33:04 +08:00
ext_list = [ ]
for ext in extensions :
ext_list . extend ( ext . replace ( ' ' , ' ' ) . split ( ' , ' ) )
for i , ext in enumerate ( ext_list ) :
if not ext . startswith ( ' . ' ) :
ext_list [ i ] = ' . %s ' % ext_list [ i ]
return set ( [ x for x in ext_list if x . strip ( ' . ' ) not in ignored ] )
2013-01-17 03:21:47 +08:00
2008-07-06 14:39:44 +08:00
2011-01-22 05:00:39 +08:00
class Command ( NoArgsCommand ) :
option_list = NoArgsCommand . option_list + (
2012-06-07 17:23:25 +08:00
make_option ( ' --locale ' , ' -l ' , default = None , dest = ' locale ' , action = ' append ' ,
2013-01-17 20:33:04 +08:00
help = ' Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
' Can be used multiple times, accepts a comma-separated list of locale names. ' ) ,
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 ' ,
2013-01-17 20:33:04 +08:00
default = True , help = " Don ' t ignore the common glob-style patterns ' CVS ' , ' .* ' , ' *~ ' and ' *.pyc ' . " ) ,
2010-11-04 20:08:37 +08:00
make_option ( ' --no-wrap ' , action = ' store_true ' , dest = ' no_wrap ' ,
2013-01-19 23:06:52 +08:00
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 ' ,
2013-01-19 23:06:52 +08:00
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 ' ,
2013-01-19 23:06:52 +08:00
default = False , help = " Remove obsolete message strings. " ) ,
2013-01-17 02:36:22 +08:00
make_option ( ' --keep-pot ' , action = ' store_true ' , dest = ' keep_pot ' ,
default = False , help = " Keep .pot file after making messages. Useful when debugging. " ) ,
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 ' )
2013-01-17 20:33:04 +08:00
self . domain = options . get ( ' domain ' )
self . verbosity = int ( options . get ( ' verbosity ' ) )
2008-07-06 14:39:44 +08:00
process_all = options . get ( ' all ' )
2010-02-16 20:12:13 +08:00
extensions = options . get ( ' extensions ' )
2013-01-17 20:33:04 +08:00
self . 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 ' ) :
2013-01-17 20:33:04 +08:00
ignore_patterns + = [ ' CVS ' , ' .* ' , ' *~ ' , ' *.pyc ' ]
self . ignore_patterns = list ( set ( ignore_patterns ) )
self . wrap = ' --no-wrap ' if options . get ( ' no_wrap ' ) else ' '
self . location = ' --no-location ' if options . get ( ' no_location ' ) else ' '
self . no_obsolete = options . get ( ' no_obsolete ' )
self . keep_pot = options . get ( ' keep_pot ' )
if self . domain not in ( ' django ' , ' djangojs ' ) :
raise CommandError ( " currently makemessages only supports domains "
" ' django ' and ' djangojs ' " )
if self . domain == ' djangojs ' :
2012-02-05 06:12:58 +08:00
exts = extensions if extensions else [ ' js ' ]
2008-08-09 00:41:55 +08:00
else :
2012-02-05 06:12:58 +08:00
exts = extensions if extensions else [ ' html ' , ' txt ' ]
2013-01-17 20:33:04 +08:00
self . extensions = handle_extensions ( exts )
2008-08-09 00:41:55 +08:00
2013-01-17 20:33:04 +08:00
if ( locale is None and not process_all ) or self . domain is None :
raise CommandError ( " Type ' %s help %s ' for usage information. " % (
os . path . basename ( sys . argv [ 0 ] ) , sys . argv [ 1 ] ) )
if self . verbosity > 1 :
2012-02-05 02:27:24 +08:00
self . stdout . write ( ' examining files with the extensions: %s \n '
2013-01-17 20:33:04 +08:00
% get_text_list ( list ( self . extensions ) , ' and ' ) )
2008-07-06 14:39:44 +08:00
2013-01-17 20:33:04 +08:00
# Need to ensure that the i18n framework is enabled
from django . conf import settings
if settings . configured :
settings . USE_I18N = True
else :
settings . configure ( USE_I18N = True )
self . invoked_for_django = False
if os . path . isdir ( os . path . join ( ' conf ' , ' locale ' ) ) :
2013-01-26 00:58:37 +08:00
localedir = os . path . abspath ( os . path . join ( ' conf ' , ' locale ' ) )
2013-01-17 20:33:04 +08:00
self . invoked_for_django = True
# Ignoring all contrib apps
self . ignore_patterns + = [ ' contrib/* ' ]
2013-01-26 00:58:37 +08:00
elif os . path . isdir ( ' locale ' ) :
localedir = os . path . abspath ( ' locale ' )
2013-01-17 20:33:04 +08:00
else :
2013-01-26 00:58:37 +08:00
raise CommandError ( " This script should be run from the Django Git "
" tree or your project or app tree. If you did indeed run it "
" from the Git 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. " )
2013-01-17 20:33:04 +08:00
# We require gettext version 0.15 or newer.
output , errors , status = _popen ( ' xgettext --version ' )
if status != STATUS_OK :
raise CommandError ( " Error running xgettext. Note that Django "
" internationalization requires GNU gettext 0.15 or newer. " )
match = re . search ( r ' (?P<major> \ d+) \ .(?P<minor> \ d+) ' , output )
if match :
xversion = ( int ( match . group ( ' major ' ) ) , int ( match . group ( ' minor ' ) ) )
if xversion < ( 0 , 15 ) :
raise CommandError ( " Django internationalization requires GNU "
" gettext 0.15 or newer. You are using version %s , please "
" upgrade your gettext toolset. " % match . group ( ) )
2013-01-26 00:58:37 +08:00
potfile = self . build_pot_file ( localedir )
2013-01-17 20:33:04 +08:00
2013-01-26 00:58:37 +08:00
# Build po files for each selected locale
locales = [ ]
if locale is not None :
locales + = locale . split ( ' , ' ) if not isinstance ( locale , list ) else locale
elif process_all :
locale_dirs = filter ( os . path . isdir , glob . glob ( ' %s /* ' % localedir ) )
locales = [ os . path . basename ( l ) for l in locale_dirs ]
2013-01-17 20:33:04 +08:00
2013-01-26 00:58:37 +08:00
try :
2013-01-17 20:33:04 +08:00
for locale in locales :
if self . verbosity > 0 :
2013-01-19 23:06:52 +08:00
self . stdout . write ( " processing locale %s \n " % locale )
2013-01-26 00:58:37 +08:00
self . write_po_file ( potfile , locale )
2013-01-17 20:33:04 +08:00
finally :
2013-01-26 00:58:37 +08:00
if not self . keep_pot and os . path . exists ( potfile ) :
os . unlink ( potfile )
2013-01-17 20:33:04 +08:00
def build_pot_file ( self , localedir ) :
file_list = self . find_files ( " . " )
potfile = os . path . join ( localedir , ' %s .pot ' % str ( self . domain ) )
if os . path . exists ( potfile ) :
# Remove a previous undeleted potfile, if any
os . unlink ( potfile )
for f in file_list :
f . process ( self , potfile , self . domain , self . keep_pot )
return potfile
def find_files ( self , root ) :
"""
2013-01-26 00:58:37 +08:00
Helper method to get all files in the given root .
2013-01-17 20:33:04 +08:00
"""
def is_ignored ( path , ignore_patterns ) :
"""
Check if the given path should be ignored or not .
"""
for pattern in ignore_patterns :
if fnmatch . fnmatchcase ( path , pattern ) :
return True
return False
dir_suffix = ' %s * ' % os . sep
norm_patterns = [ p [ : - len ( dir_suffix ) ] if p . endswith ( dir_suffix ) else p for p in self . ignore_patterns ]
all_files = [ ]
for dirpath , dirnames , filenames in os . walk ( root , topdown = True , followlinks = self . symlinks ) :
for dirname in dirnames [ : ] :
if is_ignored ( os . path . normpath ( os . path . join ( dirpath , dirname ) ) , norm_patterns ) :
dirnames . remove ( dirname )
if self . verbosity > 1 :
self . stdout . write ( ' ignoring directory %s \n ' % dirname )
for filename in filenames :
2013-01-26 00:58:37 +08:00
if is_ignored ( os . path . normpath ( os . path . join ( dirpath , filename ) ) , self . ignore_patterns ) :
2013-01-17 20:33:04 +08:00
if self . verbosity > 1 :
self . stdout . write ( ' ignoring file %s in %s \n ' % ( filename , dirpath ) )
else :
2013-01-26 00:58:37 +08:00
all_files . append ( TranslatableFile ( dirpath , filename ) )
2013-01-17 20:33:04 +08:00
return sorted ( all_files )
def write_po_file ( self , potfile , locale ) :
"""
Creates or updates the PO file for self . domain and : param locale : .
Uses contents of the existing : param potfile : .
2013-01-26 00:58:37 +08:00
Uses mguniq , msgmerge , and msgattrib GNU gettext utilities .
2013-01-17 20:33:04 +08:00
"""
2013-01-26 00:58:37 +08:00
msgs , errors , status = _popen ( ' msguniq %s %s --to-code=utf-8 " %s " ' %
( self . wrap , self . location , potfile ) )
if errors :
if status != STATUS_OK :
raise CommandError (
" errors happened while running msguniq \n %s " % errors )
elif self . verbosity > 0 :
self . stdout . write ( errors )
2013-01-17 20:33:04 +08:00
basedir = os . path . join ( os . path . dirname ( potfile ) , locale , ' LC_MESSAGES ' )
if not os . path . isdir ( basedir ) :
os . makedirs ( basedir )
pofile = os . path . join ( basedir , ' %s .po ' % str ( self . domain ) )
if os . path . exists ( pofile ) :
2013-01-26 00:58:37 +08:00
with open ( potfile , ' w ' ) as fp :
fp . write ( msgs )
2013-01-17 20:33:04 +08:00
msgs , errors , status = _popen ( ' msgmerge %s %s -q " %s " " %s " ' %
( self . wrap , self . location , pofile , potfile ) )
if errors :
if status != STATUS_OK :
raise CommandError (
" errors happened while running msgmerge \n %s " % errors )
elif self . verbosity > 0 :
self . stdout . write ( errors )
2013-01-26 00:58:37 +08:00
elif not self . invoked_for_django :
msgs = self . copy_plural_forms ( msgs , locale )
2013-01-17 20:33:04 +08:00
msgs = msgs . replace (
" #. #-#-#-#-# %s .pot (PACKAGE VERSION) #-#-#-#-# \n " % self . domain , " " )
with open ( pofile , ' w ' ) as fp :
fp . write ( msgs )
if self . no_obsolete :
msgs , errors , status = _popen (
' msgattrib %s %s -o " %s " --no-obsolete " %s " ' %
2013-01-19 23:06:52 +08:00
( self . wrap , self . location , pofile , pofile ) )
2013-01-17 20:33:04 +08:00
if errors :
if status != STATUS_OK :
raise CommandError (
" errors happened while running msgattrib \n %s " % errors )
elif self . verbosity > 0 :
self . stdout . write ( errors )
def copy_plural_forms ( self , msgs , locale ) :
"""
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 self . 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 ) :
with open ( django_po , ' rU ' ) as fp :
m = plural_forms_re . search ( fp . read ( ) )
if m :
if self . verbosity > 1 :
self . stdout . write ( " copying plural forms: %s \n " % m . group ( ' value ' ) )
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