Ticket #16493: makemessages.py

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