root/galaxy-central/eggs/Babel-0.9.4-py2.6.egg/babel/messages/frontend.py

リビジョン 3, 48.2 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2007 Edgewall Software
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at http://babel.edgewall.org/wiki/License.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at http://babel.edgewall.org/log/.
14
15"""Frontends for the message extraction functionality."""
16
17from ConfigParser import RawConfigParser
18from datetime import datetime
19from distutils import log
20from distutils.cmd import Command
21from distutils.errors import DistutilsOptionError, DistutilsSetupError
22from locale import getpreferredencoding
23import logging
24from optparse import OptionParser
25import os
26import re
27import shutil
28from StringIO import StringIO
29import sys
30import tempfile
31
32from babel import __version__ as VERSION
33from babel import Locale, localedata
34from babel.core import UnknownLocaleError
35from babel.messages.catalog import Catalog
36from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \
37                                   DEFAULT_MAPPING
38from babel.messages.mofile import write_mo
39from babel.messages.pofile import read_po, write_po
40from babel.messages.plurals import PLURALS
41from babel.util import odict, LOCALTZ
42
43__all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages',
44           'init_catalog', 'check_message_extractors', 'update_catalog']
45__docformat__ = 'restructuredtext en'
46
47
48class compile_catalog(Command):
49    """Catalog compilation command for use in ``setup.py`` scripts.
50
51    If correctly installed, this command is available to Setuptools-using
52    setup scripts automatically. For projects using plain old ``distutils``,
53    the command needs to be registered explicitly in ``setup.py``::
54
55        from babel.messages.frontend import compile_catalog
56
57        setup(
58            ...
59            cmdclass = {'compile_catalog': compile_catalog}
60        )
61
62    :since: version 0.9
63    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
64    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
65    """
66
67    description = 'compile message catalogs to binary MO files'
68    user_options = [
69        ('domain=', 'D',
70         "domain of PO file (default 'messages')"),
71        ('directory=', 'd',
72         'path to base directory containing the catalogs'),
73        ('input-file=', 'i',
74         'name of the input file'),
75        ('output-file=', 'o',
76         "name of the output file (default "
77         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
78        ('locale=', 'l',
79         'locale of the catalog to compile'),
80        ('use-fuzzy', 'f',
81         'also include fuzzy translations'),
82        ('statistics', None,
83         'print statistics about translations')
84    ]
85    boolean_options = ['use-fuzzy', 'statistics']
86
87    def initialize_options(self):
88        self.domain = 'messages'
89        self.directory = None
90        self.input_file = None
91        self.output_file = None
92        self.locale = None
93        self.use_fuzzy = False
94        self.statistics = False
95
96    def finalize_options(self):
97        if not self.input_file and not self.directory:
98            raise DistutilsOptionError('you must specify either the input file '
99                                       'or the base directory')
100        if not self.output_file and not self.directory:
101            raise DistutilsOptionError('you must specify either the input file '
102                                       'or the base directory')
103
104    def run(self):
105        po_files = []
106        mo_files = []
107
108        if not self.input_file:
109            if self.locale:
110                po_files.append((self.locale,
111                                 os.path.join(self.directory, self.locale,
112                                              'LC_MESSAGES',
113                                              self.domain + '.po')))
114                mo_files.append(os.path.join(self.directory, self.locale,
115                                             'LC_MESSAGES',
116                                             self.domain + '.mo'))
117            else:
118                for locale in os.listdir(self.directory):
119                    po_file = os.path.join(self.directory, locale,
120                                           'LC_MESSAGES', self.domain + '.po')
121                    if os.path.exists(po_file):
122                        po_files.append((locale, po_file))
123                        mo_files.append(os.path.join(self.directory, locale,
124                                                     'LC_MESSAGES',
125                                                     self.domain + '.mo'))
126        else:
127            po_files.append((self.locale, self.input_file))
128            if self.output_file:
129                mo_files.append(self.output_file)
130            else:
131                mo_files.append(os.path.join(self.directory, self.locale,
132                                             'LC_MESSAGES',
133                                             self.domain + '.mo'))
134
135        if not po_files:
136            raise DistutilsOptionError('no message catalogs found')
137
138        for idx, (locale, po_file) in enumerate(po_files):
139            mo_file = mo_files[idx]
140            infile = open(po_file, 'r')
141            try:
142                catalog = read_po(infile, locale)
143            finally:
144                infile.close()
145
146            if self.statistics:
147                translated = 0
148                for message in list(catalog)[1:]:
149                    if message.string:
150                        translated +=1
151                percentage = 0
152                if len(catalog):
153                    percentage = translated * 100 // len(catalog)
154                log.info('%d of %d messages (%d%%) translated in %r',
155                         translated, len(catalog), percentage, po_file)
156
157            if catalog.fuzzy and not self.use_fuzzy:
158                log.warn('catalog %r is marked as fuzzy, skipping', po_file)
159                continue
160
161            for message, errors in catalog.check():
162                for error in errors:
163                    log.error('error: %s:%d: %s', po_file, message.lineno,
164                              error)
165
166            log.info('compiling catalog %r to %r', po_file, mo_file)
167
168            outfile = open(mo_file, 'wb')
169            try:
170                write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy)
171            finally:
172                outfile.close()
173
174
175class extract_messages(Command):
176    """Message extraction command for use in ``setup.py`` scripts.
177
178    If correctly installed, this command is available to Setuptools-using
179    setup scripts automatically. For projects using plain old ``distutils``,
180    the command needs to be registered explicitly in ``setup.py``::
181
182        from babel.messages.frontend import extract_messages
183
184        setup(
185            ...
186            cmdclass = {'extract_messages': extract_messages}
187        )
188
189    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
190    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
191    """
192
193    description = 'extract localizable strings from the project code'
194    user_options = [
195        ('charset=', None,
196         'charset to use in the output file'),
197        ('keywords=', 'k',
198         'space-separated list of keywords to look for in addition to the '
199         'defaults'),
200        ('no-default-keywords', None,
201         'do not include the default keywords'),
202        ('mapping-file=', 'F',
203         'path to the mapping configuration file'),
204        ('no-location', None,
205         'do not include location comments with filename and line number'),
206        ('omit-header', None,
207         'do not include msgid "" entry in header'),
208        ('output-file=', 'o',
209         'name of the output file'),
210        ('width=', 'w',
211         'set output line width (default 76)'),
212        ('no-wrap', None,
213         'do not break long message lines, longer than the output line width, '
214         'into several lines'),
215        ('sort-output', None,
216         'generate sorted output (default False)'),
217        ('sort-by-file', None,
218         'sort output by file location (default False)'),
219        ('msgid-bugs-address=', None,
220         'set report address for msgid'),
221        ('copyright-holder=', None,
222         'set copyright holder in output'),
223        ('add-comments=', 'c',
224         'place comment block with TAG (or those preceding keyword lines) in '
225         'output file. Seperate multiple TAGs with commas(,)'),
226        ('strip-comments', None,
227         'strip the comment TAGs from the comments.'),
228        ('input-dirs=', None,
229         'directories that should be scanned for messages'),
230    ]
231    boolean_options = [
232        'no-default-keywords', 'no-location', 'omit-header', 'no-wrap',
233        'sort-output', 'sort-by-file', 'strip-comments'
234    ]
235
236    def initialize_options(self):
237        self.charset = 'utf-8'
238        self.keywords = ''
239        self._keywords = DEFAULT_KEYWORDS.copy()
240        self.no_default_keywords = False
241        self.mapping_file = None
242        self.no_location = False
243        self.omit_header = False
244        self.output_file = None
245        self.input_dirs = None
246        self.width = 76
247        self.no_wrap = False
248        self.sort_output = False
249        self.sort_by_file = False
250        self.msgid_bugs_address = None
251        self.copyright_holder = None
252        self.add_comments = None
253        self._add_comments = []
254        self.strip_comments = False
255
256    def finalize_options(self):
257        if self.no_default_keywords and not self.keywords:
258            raise DistutilsOptionError('you must specify new keywords if you '
259                                       'disable the default ones')
260        if self.no_default_keywords:
261            self._keywords = {}
262        if self.keywords:
263            self._keywords.update(parse_keywords(self.keywords.split()))
264
265        if not self.output_file:
266            raise DistutilsOptionError('no output file specified')
267        if self.no_wrap and self.width:
268            raise DistutilsOptionError("'--no-wrap' and '--width' are mutually "
269                                       "exclusive")
270        if self.no_wrap:
271            self.width = None
272        else:
273            self.width = int(self.width)
274
275        if self.sort_output and self.sort_by_file:
276            raise DistutilsOptionError("'--sort-output' and '--sort-by-file' "
277                                       "are mutually exclusive")
278
279        if not self.input_dirs:
280            self.input_dirs = dict.fromkeys([k.split('.',1)[0]
281                for k in self.distribution.packages
282            ]).keys()
283
284        if self.add_comments:
285            self._add_comments = self.add_comments.split(',')
286
287    def run(self):
288        mappings = self._get_mappings()
289        outfile = open(self.output_file, 'w')
290        try:
291            catalog = Catalog(project=self.distribution.get_name(),
292                              version=self.distribution.get_version(),
293                              msgid_bugs_address=self.msgid_bugs_address,
294                              copyright_holder=self.copyright_holder,
295                              charset=self.charset)
296
297            for dirname, (method_map, options_map) in mappings.items():
298                def callback(filename, method, options):
299                    if method == 'ignore':
300                        return
301                    filepath = os.path.normpath(os.path.join(dirname, filename))
302                    optstr = ''
303                    if options:
304                        optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
305                                                      k, v in options.items()])
306                    log.info('extracting messages from %s%s', filepath, optstr)
307
308                extracted = extract_from_dir(dirname, method_map, options_map,
309                                             keywords=self._keywords,
310                                             comment_tags=self._add_comments,
311                                             callback=callback,
312                                             strip_comment_tags=
313                                                self.strip_comments)
314                for filename, lineno, message, comments in extracted:
315                    filepath = os.path.normpath(os.path.join(dirname, filename))
316                    catalog.add(message, None, [(filepath, lineno)],
317                                auto_comments=comments)
318
319            log.info('writing PO template file to %s' % self.output_file)
320            write_po(outfile, catalog, width=self.width,
321                     no_location=self.no_location,
322                     omit_header=self.omit_header,
323                     sort_output=self.sort_output,
324                     sort_by_file=self.sort_by_file)
325        finally:
326            outfile.close()
327
328    def _get_mappings(self):
329        mappings = {}
330
331        if self.mapping_file:
332            fileobj = open(self.mapping_file, 'U')
333            try:
334                method_map, options_map = parse_mapping(fileobj)
335                for dirname in self.input_dirs:
336                    mappings[dirname] = method_map, options_map
337            finally:
338                fileobj.close()
339
340        elif getattr(self.distribution, 'message_extractors', None):
341            message_extractors = self.distribution.message_extractors
342            for dirname, mapping in message_extractors.items():
343                if isinstance(mapping, basestring):
344                    method_map, options_map = parse_mapping(StringIO(mapping))
345                else:
346                    method_map, options_map = [], {}
347                    for pattern, method, options in mapping:
348                        method_map.append((pattern, method))
349                        options_map[pattern] = options or {}
350                mappings[dirname] = method_map, options_map
351
352        else:
353            for dirname in self.input_dirs:
354                mappings[dirname] = DEFAULT_MAPPING, {}
355
356        return mappings
357
358
359def check_message_extractors(dist, name, value):
360    """Validate the ``message_extractors`` keyword argument to ``setup()``.
361
362    :param dist: the distutils/setuptools ``Distribution`` object
363    :param name: the name of the keyword argument (should always be
364                 "message_extractors")
365    :param value: the value of the keyword argument
366    :raise `DistutilsSetupError`: if the value is not valid
367    :see: `Adding setup() arguments
368           <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_
369    """
370    assert name == 'message_extractors'
371    if not isinstance(value, dict):
372        raise DistutilsSetupError('the value of the "message_extractors" '
373                                  'parameter must be a dictionary')
374
375
376class init_catalog(Command):
377    """New catalog initialization command for use in ``setup.py`` scripts.
378
379    If correctly installed, this command is available to Setuptools-using
380    setup scripts automatically. For projects using plain old ``distutils``,
381    the command needs to be registered explicitly in ``setup.py``::
382
383        from babel.messages.frontend import init_catalog
384
385        setup(
386            ...
387            cmdclass = {'init_catalog': init_catalog}
388        )
389
390    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
391    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
392    """
393
394    description = 'create a new catalog based on a POT file'
395    user_options = [
396        ('domain=', 'D',
397         "domain of PO file (default 'messages')"),
398        ('input-file=', 'i',
399         'name of the input file'),
400        ('output-dir=', 'd',
401         'path to output directory'),
402        ('output-file=', 'o',
403         "name of the output file (default "
404         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
405        ('locale=', 'l',
406         'locale for the new localized catalog'),
407    ]
408
409    def initialize_options(self):
410        self.output_dir = None
411        self.output_file = None
412        self.input_file = None
413        self.locale = None
414        self.domain = 'messages'
415
416    def finalize_options(self):
417        if not self.input_file:
418            raise DistutilsOptionError('you must specify the input file')
419
420        if not self.locale:
421            raise DistutilsOptionError('you must provide a locale for the '
422                                       'new catalog')
423        try:
424            self._locale = Locale.parse(self.locale)
425        except UnknownLocaleError, e:
426            raise DistutilsOptionError(e)
427
428        if not self.output_file and not self.output_dir:
429            raise DistutilsOptionError('you must specify the output directory')
430        if not self.output_file:
431            self.output_file = os.path.join(self.output_dir, self.locale,
432                                            'LC_MESSAGES', self.domain + '.po')
433
434        if not os.path.exists(os.path.dirname(self.output_file)):
435            os.makedirs(os.path.dirname(self.output_file))
436
437    def run(self):
438        log.info('creating catalog %r based on %r', self.output_file,
439                 self.input_file)
440
441        infile = open(self.input_file, 'r')
442        try:
443            # Although reading from the catalog template, read_po must be fed
444            # the locale in order to correcly calculate plurals
445            catalog = read_po(infile, locale=self.locale)
446        finally:
447            infile.close()
448
449        catalog.locale = self._locale
450        catalog.fuzzy = False
451
452        outfile = open(self.output_file, 'w')
453        try:
454            write_po(outfile, catalog)
455        finally:
456            outfile.close()
457
458
459class update_catalog(Command):
460    """Catalog merging command for use in ``setup.py`` scripts.
461
462    If correctly installed, this command is available to Setuptools-using
463    setup scripts automatically. For projects using plain old ``distutils``,
464    the command needs to be registered explicitly in ``setup.py``::
465
466        from babel.messages.frontend import update_catalog
467
468        setup(
469            ...
470            cmdclass = {'update_catalog': update_catalog}
471        )
472
473    :since: version 0.9
474    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
475    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
476    """
477
478    description = 'update message catalogs from a POT file'
479    user_options = [
480        ('domain=', 'D',
481         "domain of PO file (default 'messages')"),
482        ('input-file=', 'i',
483         'name of the input file'),
484        ('output-dir=', 'd',
485         'path to base directory containing the catalogs'),
486        ('output-file=', 'o',
487         "name of the output file (default "
488         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
489        ('locale=', 'l',
490         'locale of the catalog to compile'),
491        ('ignore-obsolete=', None,
492         'whether to omit obsolete messages from the output'),
493        ('no-fuzzy-matching', 'N',
494         'do not use fuzzy matching'),
495        ('previous', None,
496         'keep previous msgids of translated messages')
497    ]
498    boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous']
499
500    def initialize_options(self):
501        self.domain = 'messages'
502        self.input_file = None
503        self.output_dir = None
504        self.output_file = None
505        self.locale = None
506        self.ignore_obsolete = False
507        self.no_fuzzy_matching = False
508        self.previous = False
509
510    def finalize_options(self):
511        if not self.input_file:
512            raise DistutilsOptionError('you must specify the input file')
513        if not self.output_file and not self.output_dir:
514            raise DistutilsOptionError('you must specify the output file or '
515                                       'directory')
516        if self.output_file and not self.locale:
517            raise DistutilsOptionError('you must specify the locale')
518        if self.no_fuzzy_matching and self.previous:
519            self.previous = False
520
521    def run(self):
522        po_files = []
523        if not self.output_file:
524            if self.locale:
525                po_files.append((self.locale,
526                                 os.path.join(self.output_dir, self.locale,
527                                              'LC_MESSAGES',
528                                              self.domain + '.po')))
529            else:
530                for locale in os.listdir(self.output_dir):
531                    po_file = os.path.join(self.output_dir, locale,
532                                           'LC_MESSAGES',
533                                           self.domain + '.po')
534                    if os.path.exists(po_file):
535                        po_files.append((locale, po_file))
536        else:
537            po_files.append((self.locale, self.output_file))
538
539        domain = self.domain
540        if not domain:
541            domain = os.path.splitext(os.path.basename(self.input_file))[0]
542
543        infile = open(self.input_file, 'U')
544        try:
545            template = read_po(infile)
546        finally:
547            infile.close()
548
549        if not po_files:
550            raise DistutilsOptionError('no message catalogs found')
551
552        for locale, filename in po_files:
553            log.info('updating catalog %r based on %r', filename,
554                     self.input_file)
555            infile = open(filename, 'U')
556            try:
557                catalog = read_po(infile, locale=locale, domain=domain)
558            finally:
559                infile.close()
560
561            catalog.update(template, self.no_fuzzy_matching)
562
563            tmpname = os.path.join(os.path.dirname(filename),
564                                   tempfile.gettempprefix() +
565                                   os.path.basename(filename))
566            tmpfile = open(tmpname, 'w')
567            try:
568                try:
569                    write_po(tmpfile, catalog,
570                             ignore_obsolete=self.ignore_obsolete,
571                             include_previous=self.previous)
572                finally:
573                    tmpfile.close()
574            except:
575                os.remove(tmpname)
576                raise
577
578            try:
579                os.rename(tmpname, filename)
580            except OSError:
581                # We're probably on Windows, which doesn't support atomic
582                # renames, at least not through Python
583                # If the error is in fact due to a permissions problem, that
584                # same error is going to be raised from one of the following
585                # operations
586                os.remove(filename)
587                shutil.copy(tmpname, filename)
588                os.remove(tmpname)
589
590
591class CommandLineInterface(object):
592    """Command-line interface.
593
594    This class provides a simple command-line interface to the message
595    extraction and PO file generation functionality.
596    """
597
598    usage = '%%prog %s [options] %s'
599    version = '%%prog %s' % VERSION
600    commands = {
601        'compile': 'compile message catalogs to MO files',
602        'extract': 'extract messages from source files and generate a POT file',
603        'init':    'create new message catalogs from a POT file',
604        'update':  'update existing message catalogs from a POT file'
605    }
606
607    def run(self, argv=sys.argv):
608        """Main entry point of the command-line interface.
609
610        :param argv: list of arguments passed on the command-line
611        """
612        self.parser = OptionParser(usage=self.usage % ('command', '[args]'),
613                                   version=self.version)
614        self.parser.disable_interspersed_args()
615        self.parser.print_help = self._help
616        self.parser.add_option('--list-locales', dest='list_locales',
617                               action='store_true',
618                               help="print all known locales and exit")
619        self.parser.add_option('-v', '--verbose', action='store_const',
620                               dest='loglevel', const=logging.DEBUG,
621                               help='print as much as possible')
622        self.parser.add_option('-q', '--quiet', action='store_const',
623                               dest='loglevel', const=logging.ERROR,
624                               help='print as little as possible')
625        self.parser.set_defaults(list_locales=False, loglevel=logging.INFO)
626
627        options, args = self.parser.parse_args(argv[1:])
628
629        # Configure logging
630        self.log = logging.getLogger('babel')
631        self.log.setLevel(options.loglevel)
632        handler = logging.StreamHandler()
633        handler.setLevel(options.loglevel)
634        formatter = logging.Formatter('%(message)s')
635        handler.setFormatter(formatter)
636        self.log.addHandler(handler)
637
638        if options.list_locales:
639            identifiers = localedata.list()
640            longest = max([len(identifier) for identifier in identifiers])
641            format = u'%%-%ds %%s' % (longest + 1)
642            for identifier in localedata.list():
643                locale = Locale.parse(identifier)
644                output = format % (identifier, locale.english_name)
645                print output.encode(sys.stdout.encoding or
646                                    getpreferredencoding() or
647                                    'ascii', 'replace')
648            return 0
649
650        if not args:
651            self.parser.error('incorrect number of arguments')
652
653        cmdname = args[0]
654        if cmdname not in self.commands:
655            self.parser.error('unknown command "%s"' % cmdname)
656
657        return getattr(self, cmdname)(args[1:])
658
659    def _help(self):
660        print self.parser.format_help()
661        print "commands:"
662        longest = max([len(command) for command in self.commands])
663        format = "  %%-%ds %%s" % max(8, longest + 1)
664        commands = self.commands.items()
665        commands.sort()
666        for name, description in commands:
667            print format % (name, description)
668
669    def compile(self, argv):
670        """Subcommand for compiling a message catalog to a MO file.
671
672        :param argv: the command arguments
673        :since: version 0.9
674        """
675        parser = OptionParser(usage=self.usage % ('compile', ''),
676                              description=self.commands['compile'])
677        parser.add_option('--domain', '-D', dest='domain',
678                          help="domain of MO and PO files (default '%default')")
679        parser.add_option('--directory', '-d', dest='directory',
680                          metavar='DIR', help='base directory of catalog files')
681        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
682                          help='locale of the catalog')
683        parser.add_option('--input-file', '-i', dest='input_file',
684                          metavar='FILE', help='name of the input file')
685        parser.add_option('--output-file', '-o', dest='output_file',
686                          metavar='FILE',
687                          help="name of the output file (default "
688                               "'<output_dir>/<locale>/LC_MESSAGES/"
689                               "<domain>.mo')")
690        parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy',
691                          action='store_true',
692                          help='also include fuzzy translations (default '
693                               '%default)')
694        parser.add_option('--statistics', dest='statistics',
695                          action='store_true',
696                          help='print statistics about translations')
697
698        parser.set_defaults(domain='messages', use_fuzzy=False,
699                            compile_all=False, statistics=False)
700        options, args = parser.parse_args(argv)
701
702        po_files = []
703        mo_files = []
704        if not options.input_file:
705            if not options.directory:
706                parser.error('you must specify either the input file or the '
707                             'base directory')
708            if options.locale:
709                po_files.append((options.locale,
710                                 os.path.join(options.directory,
711                                              options.locale, 'LC_MESSAGES',
712                                              options.domain + '.po')))
713                mo_files.append(os.path.join(options.directory, options.locale,
714                                             'LC_MESSAGES',
715                                             options.domain + '.mo'))
716            else:
717                for locale in os.listdir(options.directory):
718                    po_file = os.path.join(options.directory, locale,
719                                           'LC_MESSAGES', options.domain + '.po')
720                    if os.path.exists(po_file):
721                        po_files.append((locale, po_file))
722                        mo_files.append(os.path.join(options.directory, locale,
723                                                     'LC_MESSAGES',
724                                                     options.domain + '.mo'))
725        else:
726            po_files.append((options.locale, options.input_file))
727            if options.output_file:
728                mo_files.append(options.output_file)
729            else:
730                if not options.directory:
731                    parser.error('you must specify either the input file or '
732                                 'the base directory')
733                mo_files.append(os.path.join(options.directory, options.locale,
734                                             'LC_MESSAGES',
735                                             options.domain + '.mo'))
736        if not po_files:
737            parser.error('no message catalogs found')
738
739        for idx, (locale, po_file) in enumerate(po_files):
740            mo_file = mo_files[idx]
741            infile = open(po_file, 'r')
742            try:
743                catalog = read_po(infile, locale)
744            finally:
745                infile.close()
746
747            if options.statistics:
748                translated = 0
749                for message in list(catalog)[1:]:
750                    if message.string:
751                        translated +=1
752                percentage = 0
753                if len(catalog):
754                    percentage = translated * 100 // len(catalog)
755                self.log.info("%d of %d messages (%d%%) translated in %r",
756                              translated, len(catalog), percentage, po_file)
757
758            if catalog.fuzzy and not options.use_fuzzy:
759                self.log.warn('catalog %r is marked as fuzzy, skipping',
760                              po_file)
761                continue
762
763            for message, errors in catalog.check():
764                for error in errors:
765                    self.log.error('error: %s:%d: %s', po_file, message.lineno,
766                                   error)
767
768            self.log.info('compiling catalog %r to %r', po_file, mo_file)
769
770            outfile = open(mo_file, 'wb')
771            try:
772                write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy)
773            finally:
774                outfile.close()
775
776    def extract(self, argv):
777        """Subcommand for extracting messages from source files and generating
778        a POT file.
779
780        :param argv: the command arguments
781        """
782        parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'),
783                              description=self.commands['extract'])
784        parser.add_option('--charset', dest='charset',
785                          help='charset to use in the output (default '
786                               '"%default")')
787        parser.add_option('-k', '--keyword', dest='keywords', action='append',
788                          help='keywords to look for in addition to the '
789                               'defaults. You can specify multiple -k flags on '
790                               'the command line.')
791        parser.add_option('--no-default-keywords', dest='no_default_keywords',
792                          action='store_true',
793                          help="do not include the default keywords")
794        parser.add_option('--mapping', '-F', dest='mapping_file',
795                          help='path to the extraction mapping file')
796        parser.add_option('--no-location', dest='no_location',
797                          action='store_true',
798                          help='do not include location comments with filename '
799                               'and line number')
800        parser.add_option('--omit-header', dest='omit_header',
801                          action='store_true',
802                          help='do not include msgid "" entry in header')
803        parser.add_option('-o', '--output', dest='output',
804                          help='path to the output POT file')
805        parser.add_option('-w', '--width', dest='width', type='int',
806                          help="set output line width (default %default)")
807        parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true',
808                          help='do not break long message lines, longer than '
809                               'the output line width, into several lines')
810        parser.add_option('--sort-output', dest='sort_output',
811                          action='store_true',
812                          help='generate sorted output (default False)')
813        parser.add_option('--sort-by-file', dest='sort_by_file',
814                          action='store_true',
815                          help='sort output by file location (default False)')
816        parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address',
817                          metavar='EMAIL@ADDRESS',
818                          help='set report address for msgid')
819        parser.add_option('--copyright-holder', dest='copyright_holder',
820                          help='set copyright holder in output')
821        parser.add_option('--add-comments', '-c', dest='comment_tags',
822                          metavar='TAG', action='append',
823                          help='place comment block with TAG (or those '
824                               'preceding keyword lines) in output file. One '
825                               'TAG per argument call')
826        parser.add_option('--strip-comment-tags', '-s',
827                          dest='strip_comment_tags', action='store_true',
828                          help='Strip the comment tags from the comments.')
829
830        parser.set_defaults(charset='utf-8', keywords=[],
831                            no_default_keywords=False, no_location=False,
832                            omit_header = False, width=76, no_wrap=False,
833                            sort_output=False, sort_by_file=False,
834                            comment_tags=[], strip_comment_tags=False)
835        options, args = parser.parse_args(argv)
836        if not args:
837            parser.error('incorrect number of arguments')
838
839        if options.output not in (None, '-'):
840            outfile = open(options.output, 'w')
841        else:
842            outfile = sys.stdout
843
844        keywords = DEFAULT_KEYWORDS.copy()
845        if options.no_default_keywords:
846            if not options.keywords:
847                parser.error('you must specify new keywords if you disable the '
848                             'default ones')
849            keywords = {}
850        if options.keywords:
851            keywords.update(parse_keywords(options.keywords))
852
853        if options.mapping_file:
854            fileobj = open(options.mapping_file, 'U')
855            try:
856                method_map, options_map = parse_mapping(fileobj)
857            finally:
858                fileobj.close()
859        else:
860            method_map = DEFAULT_MAPPING
861            options_map = {}
862
863        if options.width and options.no_wrap:
864            parser.error("'--no-wrap' and '--width' are mutually exclusive.")
865        elif not options.width and not options.no_wrap:
866            options.width = 76
867        elif not options.width and options.no_wrap:
868            options.width = 0
869
870        if options.sort_output and options.sort_by_file:
871            parser.error("'--sort-output' and '--sort-by-file' are mutually "
872                         "exclusive")
873
874        try:
875            catalog = Catalog(msgid_bugs_address=options.msgid_bugs_address,
876                              copyright_holder=options.copyright_holder,
877                              charset=options.charset)
878
879            for dirname in args:
880                if not os.path.isdir(dirname):
881                    parser.error('%r is not a directory' % dirname)
882
883                def callback(filename, method, options):
884                    if method == 'ignore':
885                        return
886                    filepath = os.path.normpath(os.path.join(dirname, filename))
887                    optstr = ''
888                    if options:
889                        optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
890                                                      k, v in options.items()])
891                    self.log.info('extracting messages from %s%s', filepath,
892                                  optstr)
893
894                extracted = extract_from_dir(dirname, method_map, options_map,
895                                             keywords, options.comment_tags,
896                                             callback=callback,
897                                             strip_comment_tags=
898                                                options.strip_comment_tags)
899                for filename, lineno, message, comments in extracted:
900                    filepath = os.path.normpath(os.path.join(dirname, filename))
901                    catalog.add(message, None, [(filepath, lineno)],
902                                auto_comments=comments)
903
904            if options.output not in (None, '-'):
905                self.log.info('writing PO template file to %s' % options.output)
906            write_po(outfile, catalog, width=options.width,
907                     no_location=options.no_location,
908                     omit_header=options.omit_header,
909                     sort_output=options.sort_output,
910                     sort_by_file=options.sort_by_file)
911        finally:
912            if options.output:
913                outfile.close()
914
915    def init(self, argv):
916        """Subcommand for creating new message catalogs from a template.
917
918        :param argv: the command arguments
919        """
920        parser = OptionParser(usage=self.usage % ('init', ''),
921                              description=self.commands['init'])
922        parser.add_option('--domain', '-D', dest='domain',
923                          help="domain of PO file (default '%default')")
924        parser.add_option('--input-file', '-i', dest='input_file',
925                          metavar='FILE', help='name of the input file')
926        parser.add_option('--output-dir', '-d', dest='output_dir',
927                          metavar='DIR', help='path to output directory')
928        parser.add_option('--output-file', '-o', dest='output_file',
929                          metavar='FILE',
930                          help="name of the output file (default "
931                               "'<output_dir>/<locale>/LC_MESSAGES/"
932                               "<domain>.po')")
933        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
934                          help='locale for the new localized catalog')
935
936        parser.set_defaults(domain='messages')
937        options, args = parser.parse_args(argv)
938
939        if not options.locale:
940            parser.error('you must provide a locale for the new catalog')
941        try:
942            locale = Locale.parse(options.locale)
943        except UnknownLocaleError, e:
944            parser.error(e)
945
946        if not options.input_file:
947            parser.error('you must specify the input file')
948
949        if not options.output_file and not options.output_dir:
950            parser.error('you must specify the output file or directory')
951
952        if not options.output_file:
953            options.output_file = os.path.join(options.output_dir,
954                                               options.locale, 'LC_MESSAGES',
955                                               options.domain + '.po')
956        if not os.path.exists(os.path.dirname(options.output_file)):
957            os.makedirs(os.path.dirname(options.output_file))
958
959        infile = open(options.input_file, 'r')
960        try:
961            # Although reading from the catalog template, read_po must be fed
962            # the locale in order to correcly calculate plurals
963            catalog = read_po(infile, locale=options.locale)
964        finally:
965            infile.close()
966
967        catalog.locale = locale
968        catalog.revision_date = datetime.now(LOCALTZ)
969
970        self.log.info('creating catalog %r based on %r', options.output_file,
971                      options.input_file)
972
973        outfile = open(options.output_file, 'w')
974        try:
975            write_po(outfile, catalog)
976        finally:
977            outfile.close()
978
979    def update(self, argv):
980        """Subcommand for updating existing message catalogs from a template.
981
982        :param argv: the command arguments
983        :since: version 0.9
984        """
985        parser = OptionParser(usage=self.usage % ('update', ''),
986                              description=self.commands['update'])
987        parser.add_option('--domain', '-D', dest='domain',
988                          help="domain of PO file (default '%default')")
989        parser.add_option('--input-file', '-i', dest='input_file',
990                          metavar='FILE', help='name of the input file')
991        parser.add_option('--output-dir', '-d', dest='output_dir',
992                          metavar='DIR', help='path to output directory')
993        parser.add_option('--output-file', '-o', dest='output_file',
994                          metavar='FILE',
995                          help="name of the output file (default "
996                               "'<output_dir>/<locale>/LC_MESSAGES/"
997                               "<domain>.po')")
998        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
999                          help='locale of the translations catalog')
1000        parser.add_option('--ignore-obsolete', dest='ignore_obsolete',
1001                          action='store_true',
1002                          help='do not include obsolete messages in the output '
1003                               '(default %default)'),
1004        parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching',
1005                          action='store_true',
1006                          help='do not use fuzzy matching (default %default)'),
1007        parser.add_option('--previous', dest='previous', action='store_true',
1008                          help='keep previous msgids of translated messages '
1009                               '(default %default)'),
1010
1011        parser.set_defaults(domain='messages', ignore_obsolete=False,
1012                            no_fuzzy_matching=False, previous=False)
1013        options, args = parser.parse_args(argv)
1014
1015        if not options.input_file:
1016            parser.error('you must specify the input file')
1017        if not options.output_file and not options.output_dir:
1018            parser.error('you must specify the output file or directory')
1019        if options.output_file and not options.locale:
1020            parser.error('you must specify the loicale')
1021        if options.no_fuzzy_matching and options.previous:
1022            options.previous = False
1023
1024        po_files = []
1025        if not options.output_file:
1026            if options.locale:
1027                po_files.append((options.locale,
1028                                 os.path.join(options.output_dir,
1029                                              options.locale, 'LC_MESSAGES',
1030                                              options.domain + '.po')))
1031            else:
1032                for locale in os.listdir(options.output_dir):
1033                    po_file = os.path.join(options.output_dir, locale,
1034                                           'LC_MESSAGES',
1035                                           options.domain + '.po')
1036                    if os.path.exists(po_file):
1037                        po_files.append((locale, po_file))
1038        else:
1039            po_files.append((options.locale, options.output_file))
1040
1041        domain = options.domain
1042        if not domain:
1043            domain = os.path.splitext(os.path.basename(options.input_file))[0]
1044
1045        infile = open(options.input_file, 'U')
1046        try:
1047            template = read_po(infile)
1048        finally:
1049            infile.close()
1050
1051        if not po_files:
1052            parser.error('no message catalogs found')
1053
1054        for locale, filename in po_files:
1055            self.log.info('updating catalog %r based on %r', filename,
1056                          options.input_file)
1057            infile = open(filename, 'U')
1058            try:
1059                catalog = read_po(infile, locale=locale, domain=domain)
1060            finally:
1061                infile.close()
1062
1063            catalog.update(template, options.no_fuzzy_matching)
1064
1065            tmpname = os.path.join(os.path.dirname(filename),
1066                                   tempfile.gettempprefix() +
1067                                   os.path.basename(filename))
1068            tmpfile = open(tmpname, 'w')
1069            try:
1070                try:
1071                    write_po(tmpfile, catalog,
1072                             ignore_obsolete=options.ignore_obsolete,
1073                             include_previous=options.previous)
1074                finally:
1075                    tmpfile.close()
1076            except:
1077                os.remove(tmpname)
1078                raise
1079
1080            try:
1081                os.rename(tmpname, filename)
1082            except OSError:
1083                # We're probably on Windows, which doesn't support atomic
1084                # renames, at least not through Python
1085                # If the error is in fact due to a permissions problem, that
1086                # same error is going to be raised from one of the following
1087                # operations
1088                os.remove(filename)
1089                shutil.copy(tmpname, filename)
1090                os.remove(tmpname)
1091
1092
1093def main():
1094    return CommandLineInterface().run(sys.argv)
1095
1096def parse_mapping(fileobj, filename=None):
1097    """Parse an extraction method mapping from a file-like object.
1098
1099    >>> buf = StringIO('''
1100    ... [extractors]
1101    ... custom = mypackage.module:myfunc
1102    ...
1103    ... # Python source files
1104    ... [python: **.py]
1105    ...
1106    ... # Genshi templates
1107    ... [genshi: **/templates/**.html]
1108    ... include_attrs =
1109    ... [genshi: **/templates/**.txt]
1110    ... template_class = genshi.template:TextTemplate
1111    ... encoding = latin-1
1112    ...
1113    ... # Some custom extractor
1114    ... [custom: **/custom/*.*]
1115    ... ''')
1116
1117    >>> method_map, options_map = parse_mapping(buf)
1118    >>> len(method_map)
1119    4
1120
1121    >>> method_map[0]
1122    ('**.py', 'python')
1123    >>> options_map['**.py']
1124    {}
1125    >>> method_map[1]
1126    ('**/templates/**.html', 'genshi')
1127    >>> options_map['**/templates/**.html']['include_attrs']
1128    ''
1129    >>> method_map[2]
1130    ('**/templates/**.txt', 'genshi')
1131    >>> options_map['**/templates/**.txt']['template_class']
1132    'genshi.template:TextTemplate'
1133    >>> options_map['**/templates/**.txt']['encoding']
1134    'latin-1'
1135
1136    >>> method_map[3]
1137    ('**/custom/*.*', 'mypackage.module:myfunc')
1138    >>> options_map['**/custom/*.*']
1139    {}
1140
1141    :param fileobj: a readable file-like object containing the configuration
1142                    text to parse
1143    :return: a `(method_map, options_map)` tuple
1144    :rtype: `tuple`
1145    :see: `extract_from_directory`
1146    """
1147    extractors = {}
1148    method_map = []
1149    options_map = {}
1150
1151    parser = RawConfigParser()
1152    parser._sections = odict(parser._sections) # We need ordered sections
1153    parser.readfp(fileobj, filename)
1154    for section in parser.sections():
1155        if section == 'extractors':
1156            extractors = dict(parser.items(section))
1157        else:
1158            method, pattern = [part.strip() for part in section.split(':', 1)]
1159            method_map.append((pattern, method))
1160            options_map[pattern] = dict(parser.items(section))
1161
1162    if extractors:
1163        for idx, (pattern, method) in enumerate(method_map):
1164            if method in extractors:
1165                method = extractors[method]
1166            method_map[idx] = (pattern, method)
1167
1168    return (method_map, options_map)
1169
1170def parse_keywords(strings=[]):
1171    """Parse keywords specifications from the given list of strings.
1172
1173    >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3'])
1174    >>> for keyword, indices in sorted(kw.items()):
1175    ...     print (keyword, indices)
1176    ('_', None)
1177    ('dgettext', (2,))
1178    ('dngettext', (2, 3))
1179    """
1180    keywords = {}
1181    for string in strings:
1182        if ':' in string:
1183            funcname, indices = string.split(':')
1184        else:
1185            funcname, indices = string, None
1186        if funcname not in keywords:
1187            if indices:
1188                indices = tuple([(int(x)) for x in indices.split(',')])
1189            keywords[funcname] = indices
1190    return keywords
1191
1192
1193if __name__ == '__main__':
1194    main()
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。