cmlenz@64: #!/usr/bin/env python cmlenz@3: # -*- coding: utf-8 -*- cmlenz@3: # cmlenz@3: # Copyright (C) 2007 Edgewall Software cmlenz@3: # All rights reserved. cmlenz@3: # cmlenz@3: # This software is licensed as described in the file COPYING, which cmlenz@3: # you should have received as part of this distribution. The terms cmlenz@3: # are also available at http://babel.edgewall.org/wiki/License. cmlenz@3: # cmlenz@3: # This software consists of voluntary contributions made by many cmlenz@3: # individuals. For the exact contribution history, see the revision cmlenz@3: # history and logs, available at http://babel.edgewall.org/log/. cmlenz@3: cmlenz@3: """Frontends for the message extraction functionality.""" cmlenz@3: cmlenz@50: from ConfigParser import RawConfigParser cmlenz@106: from datetime import datetime cmlenz@3: from distutils import log cmlenz@3: from distutils.cmd import Command cmlenz@51: from distutils.errors import DistutilsOptionError, DistutilsSetupError cmlenz@3: from optparse import OptionParser cmlenz@3: import os cmlenz@49: import re cmlenz@51: from StringIO import StringIO cmlenz@3: import sys cmlenz@3: cmlenz@3: from babel import __version__ as VERSION palgarvio@53: from babel import Locale palgarvio@53: from babel.core import UnknownLocaleError cmlenz@58: from babel.messages.catalog import Catalog cmlenz@56: from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ cmlenz@56: DEFAULT_MAPPING cmlenz@162: from babel.messages.mofile import write_mo cmlenz@106: from babel.messages.pofile import read_po, write_po cmlenz@56: from babel.messages.plurals import PLURALS cmlenz@106: from babel.util import odict, LOCALTZ cmlenz@3: cmlenz@54: __all__ = ['CommandLineInterface', 'extract_messages', cmlenz@54: 'check_message_extractors', 'main'] cmlenz@3: __docformat__ = 'restructuredtext en' cmlenz@3: cmlenz@3: cmlenz@162: class compile_catalog(Command): cmlenz@162: """Catalog compilation command for use in ``setup.py`` scripts. cmlenz@162: cmlenz@162: If correctly installed, this command is available to Setuptools-using cmlenz@162: setup scripts automatically. For projects using plain old ``distutils``, cmlenz@162: the command needs to be registered explicitly in ``setup.py``:: cmlenz@162: cmlenz@162: from babel.messages.frontend import compile_catalog cmlenz@162: cmlenz@162: setup( cmlenz@162: ... palgarvio@171: cmdclass = {'compile_catalog': compile_catalog} cmlenz@162: ) cmlenz@162: cmlenz@162: :see: `Integrating new distutils commands `_ cmlenz@162: :see: `setuptools `_ cmlenz@162: """ cmlenz@162: cmlenz@162: description = 'compile a catalog to a binary MO file' cmlenz@162: user_options = [ cmlenz@162: ('domain=', 'D', cmlenz@162: "domain of PO file (default 'messages')"), cmlenz@162: ('directory=', 'd', cmlenz@162: 'path to base directory containing the catalogs'), cmlenz@162: ('input-file=', 'i', cmlenz@162: 'name of the input file'), cmlenz@162: ('output-file=', 'o', cmlenz@162: "name of the output file (default " cmlenz@162: "'//LC_MESSAGES/.po')"), cmlenz@162: ('locale=', 'l', cmlenz@162: 'locale of the catalog to compile'), cmlenz@162: ('use-fuzzy', 'f', cmlenz@162: 'also include fuzzy translations'), palgarvio@172: ('compile-all', 'A', palgarvio@172: "compile all available PO's"), cmlenz@162: ] palgarvio@172: boolean_options = ['use-fuzzy', 'compile-all'] cmlenz@162: cmlenz@162: def initialize_options(self): cmlenz@162: self.domain = 'messages' cmlenz@162: self.directory = None cmlenz@162: self.input_file = None cmlenz@162: self.output_file = None cmlenz@162: self.locale = None cmlenz@162: self.use_fuzzy = False palgarvio@172: self.compile_all = False palgarvio@172: self._available_pos = [] cmlenz@162: cmlenz@162: def finalize_options(self): cmlenz@162: if not self.locale: palgarvio@172: if self.compile_all and not self.directory: palgarvio@172: raise DistutilsOptionError('you must specify the locale for the' palgarvio@172: ' catalog to compile') palgarvio@172: elif self.compile_all and self.directory: palgarvio@172: for locale in os.listdir(self.directory): palgarvio@172: po_path = os.path.join(self.directory, locale, palgarvio@172: 'LC_MESSAGES', self.domain + '.po') palgarvio@172: if os.path.exists(po_path): palgarvio@172: self._available_pos.append(po_path) palgarvio@172: else: palgarvio@172: raise DistutilsOptionError('you must specify the locale for the' palgarvio@172: ' catalog to compile') palgarvio@172: else: palgarvio@172: try: palgarvio@172: self._locale = Locale.parse(self.locale) palgarvio@172: except UnknownLocaleError, e: palgarvio@172: raise DistutilsOptionError(e) cmlenz@162: cmlenz@162: if not self.directory and not self.input_file: cmlenz@162: raise DistutilsOptionError('you must specify the input file') cmlenz@162: if not self.input_file: cmlenz@162: self.input_file = os.path.join(self.directory, self.locale, cmlenz@162: 'LC_MESSAGES', self.domain + '.po') cmlenz@162: cmlenz@162: if not self.directory and not self.output_file: cmlenz@162: raise DistutilsOptionError('you must specify the output file') palgarvio@172: if not self.output_file and not self.compile_all: cmlenz@162: self.output_file = os.path.join(self.directory, self.locale, cmlenz@162: 'LC_MESSAGES', self.domain + '.mo') cmlenz@162: palgarvio@172: if not self.compile_all: palgarvio@172: if not os.path.exists(os.path.dirname(self.output_file)): palgarvio@172: os.makedirs(os.path.dirname(self.output_file)) cmlenz@162: cmlenz@162: def run(self): palgarvio@172: if self.compile_all: palgarvio@172: for po in self._available_pos: palgarvio@172: log.info('compiling catalog to %s', po.replace('.po', '.mo')) palgarvio@172: palgarvio@172: infile = open(po, 'r') palgarvio@172: try: palgarvio@172: catalog = read_po(infile) palgarvio@172: finally: palgarvio@172: infile.close() palgarvio@172: palgarvio@172: outfile = open(po.replace('.po', '.mo'), 'w') palgarvio@172: try: palgarvio@172: write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) palgarvio@172: finally: palgarvio@172: outfile.close() palgarvio@172: else: palgarvio@172: log.info('compiling catalog to %s', self.output_file) cmlenz@162: palgarvio@172: infile = open(self.input_file, 'r') palgarvio@172: try: palgarvio@172: catalog = read_po(infile) palgarvio@172: finally: palgarvio@172: infile.close() palgarvio@172: palgarvio@172: outfile = open(self.output_file, 'w') palgarvio@172: try: palgarvio@172: write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) palgarvio@172: finally: palgarvio@172: outfile.close() cmlenz@162: cmlenz@162: cmlenz@3: class extract_messages(Command): cmlenz@3: """Message extraction command for use in ``setup.py`` scripts. cmlenz@54: cmlenz@3: If correctly installed, this command is available to Setuptools-using cmlenz@3: setup scripts automatically. For projects using plain old ``distutils``, cmlenz@3: the command needs to be registered explicitly in ``setup.py``:: cmlenz@54: cmlenz@56: from babel.messages.frontend import extract_messages cmlenz@54: cmlenz@3: setup( cmlenz@3: ... cmlenz@3: cmdclass = {'extract_messages': extract_messages} cmlenz@3: ) cmlenz@54: cmlenz@3: :see: `Integrating new distutils commands `_ cmlenz@3: :see: `setuptools `_ cmlenz@3: """ cmlenz@3: cmlenz@3: description = 'extract localizable strings from the project code' cmlenz@3: user_options = [ cmlenz@3: ('charset=', None, cmlenz@3: 'charset to use in the output file'), cmlenz@3: ('keywords=', 'k', palgarvio@12: 'space-separated list of keywords to look for in addition to the ' cmlenz@3: 'defaults'), palgarvio@12: ('no-default-keywords', None, cmlenz@14: 'do not include the default keywords'), cmlenz@49: ('mapping-file=', 'F', cmlenz@49: 'path to the mapping configuration file'), cmlenz@3: ('no-location', None, cmlenz@3: 'do not include location comments with filename and line number'), cmlenz@3: ('omit-header', None, cmlenz@3: 'do not include msgid "" entry in header'), cmlenz@7: ('output-file=', 'o', cmlenz@3: 'name of the output file'), palgarvio@25: ('width=', 'w', cmlenz@26: 'set output line width (default 76)'), palgarvio@25: ('no-wrap', None, cmlenz@26: 'do not break long message lines, longer than the output line width, ' cmlenz@60: 'into several lines'), palgarvio@73: ('sort-output', None, palgarvio@73: 'generate sorted output (default False)'), palgarvio@73: ('sort-by-file', None, palgarvio@73: 'sort output by file location (default False)'), palgarvio@80: ('msgid-bugs-address=', None, palgarvio@80: 'set report address for msgid'), palgarvio@81: ('copyright-holder=', None, palgarvio@81: 'set copyright holder in output'), palgarvio@82: ('add-comments=', 'c', palgarvio@82: 'place comment block with TAG (or those preceding keyword lines) in ' palgarvio@82: 'output file. Seperate multiple TAGs with commas(,)'), palgarvio@61: ('input-dirs=', None, cmlenz@59: 'directories that should be scanned for messages'), cmlenz@3: ] palgarvio@25: boolean_options = [ palgarvio@73: 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', palgarvio@73: 'sort-output', 'sort-by-file' palgarvio@25: ] cmlenz@3: cmlenz@3: def initialize_options(self): cmlenz@3: self.charset = 'utf-8' cmlenz@119: self.keywords = '' cmlenz@119: self._keywords = DEFAULT_KEYWORDS.copy() cmlenz@14: self.no_default_keywords = False cmlenz@49: self.mapping_file = None cmlenz@3: self.no_location = False cmlenz@3: self.omit_header = False cmlenz@3: self.output_file = None cmlenz@59: self.input_dirs = None cmlenz@49: self.width = 76 cmlenz@49: self.no_wrap = False palgarvio@73: self.sort_output = False palgarvio@73: self.sort_by_file = False palgarvio@80: self.msgid_bugs_address = None palgarvio@81: self.copyright_holder = None palgarvio@82: self.add_comments = None cmlenz@97: self._add_comments = [] cmlenz@3: cmlenz@3: def finalize_options(self): palgarvio@25: if self.no_default_keywords and not self.keywords: cmlenz@26: raise DistutilsOptionError('you must specify new keywords if you ' cmlenz@26: 'disable the default ones') cmlenz@14: if self.no_default_keywords: palgarvio@25: self._keywords = {} cmlenz@119: if self.keywords: palgarvio@25: self._keywords.update(parse_keywords(self.keywords.split())) cmlenz@26: cmlenz@119: if not self.output_file: cmlenz@119: raise DistutilsOptionError('no output file specified') palgarvio@25: if self.no_wrap and self.width: palgarvio@73: raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " cmlenz@26: "exclusive") cmlenz@26: if self.no_wrap: cmlenz@26: self.width = None cmlenz@26: else: palgarvio@25: self.width = int(self.width) cmlenz@97: palgarvio@73: if self.sort_output and self.sort_by_file: palgarvio@73: raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " palgarvio@73: "are mutually exclusive") cmlenz@3: cmlenz@59: if not self.input_dirs: cmlenz@59: self.input_dirs = dict.fromkeys([k.split('.',1)[0] cmlenz@59: for k in self.distribution.packages cmlenz@59: ]).keys() cmlenz@97: palgarvio@82: if self.add_comments: palgarvio@82: self._add_comments = self.add_comments.split(',') cmlenz@59: cmlenz@3: def run(self): cmlenz@64: mappings = self._get_mappings() cmlenz@3: outfile = open(self.output_file, 'w') cmlenz@3: try: cmlenz@104: catalog = Catalog(project=self.distribution.get_name(), cmlenz@104: version=self.distribution.get_version(), cmlenz@104: msgid_bugs_address=self.msgid_bugs_address, palgarvio@107: copyright_holder=self.copyright_holder, cmlenz@104: charset=self.charset) cmlenz@104: cmlenz@64: for dirname, (method_map, options_map) in mappings.items(): cmlenz@59: def callback(filename, method, options): cmlenz@59: if method == 'ignore': cmlenz@59: return cmlenz@59: filepath = os.path.normpath(os.path.join(dirname, filename)) cmlenz@59: optstr = '' cmlenz@59: if options: cmlenz@59: optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for cmlenz@59: k, v in options.items()]) cmlenz@59: log.info('extracting messages from %s%s' cmlenz@59: % (filepath, optstr)) cmlenz@49: cmlenz@59: extracted = extract_from_dir(dirname, method_map, options_map, cmlenz@119: keywords=self._keywords, cmlenz@86: comment_tags=self._add_comments, cmlenz@59: callback=callback) palgarvio@82: for filename, lineno, message, comments in extracted: cmlenz@59: filepath = os.path.normpath(os.path.join(dirname, filename)) palgarvio@82: catalog.add(message, None, [(filepath, lineno)], palgarvio@107: auto_comments=comments) cmlenz@26: palgarvio@53: log.info('writing PO template file to %s' % self.output_file) cmlenz@106: write_po(outfile, catalog, width=self.width, cmlenz@106: no_location=self.no_location, cmlenz@106: omit_header=self.omit_header, cmlenz@106: sort_output=self.sort_output, palgarvio@107: sort_by_file=self.sort_by_file) cmlenz@3: finally: cmlenz@3: outfile.close() cmlenz@3: cmlenz@64: def _get_mappings(self): cmlenz@64: mappings = {} cmlenz@64: cmlenz@64: if self.mapping_file: cmlenz@64: fileobj = open(self.mapping_file, 'U') cmlenz@64: try: cmlenz@64: method_map, options_map = parse_mapping(fileobj) cmlenz@64: for dirname in self.input_dirs: cmlenz@64: mappings[dirname] = method_map, options_map cmlenz@64: finally: cmlenz@64: fileobj.close() cmlenz@64: cmlenz@125: elif getattr(self.distribution, 'message_extractors', None): cmlenz@64: message_extractors = self.distribution.message_extractors cmlenz@64: for dirname, mapping in message_extractors.items(): cmlenz@64: if isinstance(mapping, basestring): cmlenz@64: method_map, options_map = parse_mapping(StringIO(mapping)) cmlenz@64: else: cmlenz@64: method_map, options_map = [], {} cmlenz@64: for pattern, method, options in mapping: cmlenz@64: method_map.append((pattern, method)) cmlenz@64: options_map[pattern] = options or {} cmlenz@64: mappings[dirname] = method_map, options_map cmlenz@64: cmlenz@64: else: cmlenz@64: for dirname in self.input_dirs: cmlenz@64: mappings[dirname] = DEFAULT_MAPPING, {} cmlenz@64: cmlenz@64: return mappings cmlenz@64: cmlenz@3: cmlenz@54: def check_message_extractors(dist, name, value): cmlenz@54: """Validate the ``message_extractors`` keyword argument to ``setup()``. cmlenz@49: cmlenz@54: :param dist: the distutils/setuptools ``Distribution`` object cmlenz@54: :param name: the name of the keyword argument (should always be cmlenz@54: "message_extractors") cmlenz@54: :param value: the value of the keyword argument cmlenz@54: :raise `DistutilsSetupError`: if the value is not valid cmlenz@54: :see: `Adding setup() arguments cmlenz@54: `_ cmlenz@54: """ cmlenz@54: assert name == 'message_extractors' cmlenz@64: if not isinstance(value, dict): cmlenz@64: raise DistutilsSetupError('the value of the "message_extractors" ' cmlenz@64: 'parameter must be a dictionary') cmlenz@3: cmlenz@54: palgarvio@53: class new_catalog(Command): palgarvio@53: """New catalog command for use in ``setup.py`` scripts. palgarvio@53: palgarvio@53: If correctly installed, this command is available to Setuptools-using palgarvio@53: setup scripts automatically. For projects using plain old ``distutils``, palgarvio@53: the command needs to be registered explicitly in ``setup.py``:: palgarvio@53: cmlenz@56: from babel.messages.frontend import new_catalog palgarvio@53: palgarvio@53: setup( palgarvio@53: ... palgarvio@53: cmdclass = {'new_catalog': new_catalog} palgarvio@53: ) palgarvio@53: palgarvio@53: :see: `Integrating new distutils commands `_ palgarvio@53: :see: `setuptools `_ palgarvio@53: """ palgarvio@53: palgarvio@53: description = 'create new catalogs based on a catalog template' palgarvio@53: user_options = [ palgarvio@57: ('domain=', 'D', cmlenz@106: "domain of PO file (default 'messages')"), palgarvio@53: ('input-file=', 'i', palgarvio@53: 'name of the input file'), palgarvio@53: ('output-dir=', 'd', palgarvio@53: 'path to output directory'), palgarvio@53: ('output-file=', 'o', palgarvio@53: "name of the output file (default " palgarvio@89: "'//LC_MESSAGES/.po')"), palgarvio@53: ('locale=', 'l', palgarvio@53: 'locale for the new localized catalog'), palgarvio@53: ] palgarvio@53: palgarvio@53: def initialize_options(self): palgarvio@53: self.output_dir = None palgarvio@53: self.output_file = None palgarvio@53: self.input_file = None palgarvio@53: self.locale = None cmlenz@106: self.domain = 'messages' palgarvio@53: palgarvio@53: def finalize_options(self): palgarvio@53: if not self.input_file: palgarvio@53: raise DistutilsOptionError('you must specify the input file') palgarvio@89: palgarvio@53: if not self.locale: palgarvio@53: raise DistutilsOptionError('you must provide a locale for the ' palgarvio@89: 'new catalog') cmlenz@106: try: cmlenz@106: self._locale = Locale.parse(self.locale) cmlenz@106: except UnknownLocaleError, e: cmlenz@106: raise DistutilsOptionError(e) cmlenz@54: palgarvio@53: if not self.output_file and not self.output_dir: palgarvio@53: raise DistutilsOptionError('you must specify the output directory') cmlenz@106: if not self.output_file: cmlenz@106: self.output_file = os.path.join(self.output_dir, self.locale, cmlenz@106: 'LC_MESSAGES', self.domain + '.po') palgarvio@89: palgarvio@89: if not os.path.exists(os.path.dirname(self.output_file)): palgarvio@89: os.makedirs(os.path.dirname(self.output_file)) palgarvio@53: palgarvio@53: def run(self): cmlenz@92: log.info('creating catalog %r based on %r', self.output_file, palgarvio@57: self.input_file) cmlenz@54: cmlenz@106: infile = open(self.input_file, 'r') cmlenz@106: try: cmlenz@106: catalog = read_po(infile) cmlenz@106: finally: cmlenz@106: infile.close() palgarvio@89: cmlenz@106: catalog.locale = self._locale cmlenz@106: cmlenz@106: outfile = open(self.output_file, 'w') cmlenz@106: try: cmlenz@106: write_po(outfile, catalog) cmlenz@106: finally: cmlenz@106: outfile.close() palgarvio@53: palgarvio@53: cmlenz@54: class CommandLineInterface(object): cmlenz@54: """Command-line interface. cmlenz@54: cmlenz@54: This class provides a simple command-line interface to the message cmlenz@54: extraction and PO file generation functionality. palgarvio@53: """ cmlenz@54: cmlenz@54: usage = '%%prog %s [options] %s' cmlenz@54: version = '%%prog %s' % VERSION cmlenz@163: commands = { cmlenz@162: 'compile': 'compile a message catalog to a MO file', palgarvio@65: 'extract': 'extract messages from source files and generate a POT file', cmlenz@167: 'init': 'create new message catalogs from a template', palgarvio@65: } cmlenz@54: cmlenz@54: def run(self, argv=sys.argv): cmlenz@54: """Main entry point of the command-line interface. cmlenz@54: cmlenz@54: :param argv: list of arguments passed on the command-line cmlenz@54: """ cmlenz@129: self.parser = OptionParser(usage=self.usage % ('command', '[args]'), cmlenz@54: version=self.version) palgarvio@65: self.parser.disable_interspersed_args() palgarvio@65: self.parser.print_help = self._help palgarvio@65: options, args = self.parser.parse_args(argv[1:]) cmlenz@54: if not args: palgarvio@65: self.parser.error('incorrect number of arguments') cmlenz@54: cmlenz@54: cmdname = args[0] cmlenz@54: if cmdname not in self.commands: cmlenz@129: self.parser.error('unknown command "%s"' % cmdname) cmlenz@54: cmlenz@54: getattr(self, cmdname)(args[1:]) cmlenz@54: palgarvio@65: def _help(self): palgarvio@65: print self.parser.format_help() cmlenz@129: print "commands:" palgarvio@65: longest = max([len(command) for command in self.commands]) cmlenz@129: format = " %%-%ds %%s" % max(11, longest) cmlenz@163: commands = self.commands.items() cmlenz@163: commands.sort() cmlenz@163: for name, description in commands: cmlenz@163: print format % (name, description) palgarvio@89: cmlenz@162: def compile(self, argv): cmlenz@162: """Subcommand for compiling a message catalog to a MO file. cmlenz@162: cmlenz@162: :param argv: the command arguments cmlenz@162: """ cmlenz@167: parser = OptionParser(usage=self.usage % ('init', ''), cmlenz@163: description=self.commands['init']) cmlenz@162: parser.add_option('--domain', '-D', dest='domain', cmlenz@162: help="domain of MO and PO files (default '%default')") cmlenz@162: parser.add_option('--directory', '-d', dest='directory', cmlenz@162: metavar='DIR', help='base directory of catalog files') cmlenz@162: parser.add_option('--input-file', '-i', dest='input_file', cmlenz@162: metavar='FILE', help='name of the input file') cmlenz@162: parser.add_option('--output-file', '-o', dest='output_file', cmlenz@162: metavar='FILE', cmlenz@162: help="name of the output file (default " cmlenz@162: "'//LC_MESSAGES/" cmlenz@162: ".mo')") cmlenz@162: parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', cmlenz@162: help='locale of the catalog') cmlenz@162: parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', cmlenz@162: action='store_true', cmlenz@162: help='also include fuzzy translations (default ' palgarvio@172: '%default)'), palgarvio@172: parser.add_option('--compile-all', '-A', dest='compile_all', palgarvio@172: action='store_true', palgarvio@172: help="compile all available PO's") cmlenz@162: palgarvio@172: parser.set_defaults(domain='messages', use_fuzzy=False, palgarvio@172: compile_all=False) palgarvio@172: options, args = parser.parse_args(argv) palgarvio@172: available_pos = [] cmlenz@162: if not options.locale: palgarvio@172: if options.compile_all and not options.directory: palgarvio@172: parser.error('you must provide a locale for the new catalog') palgarvio@172: elif options.compile_all and options.directory: palgarvio@172: for locale in os.listdir(options.directory): palgarvio@172: po_path = os.path.join(options.directory, locale, palgarvio@172: 'LC_MESSAGES', palgarvio@172: options.domain + '.po') palgarvio@172: if os.path.exists(po_path): palgarvio@172: available_pos.append(po_path) palgarvio@172: else: palgarvio@172: parser.error('you must provide a locale for the new catalog') palgarvio@172: else: palgarvio@172: try: palgarvio@172: locale = Locale.parse(options.locale) palgarvio@172: except UnknownLocaleError, e: palgarvio@172: parser.error(e) cmlenz@162: cmlenz@162: if not options.directory and not options.input_file: cmlenz@162: parser.error('you must specify the base directory or input file') cmlenz@162: if not options.input_file: cmlenz@162: options.input_file = os.path.join(options.directory, cmlenz@162: options.locale, 'LC_MESSAGES', cmlenz@162: options.domain + '.po') cmlenz@162: cmlenz@162: if not options.directory and not options.output_file: cmlenz@162: parser.error('you must specify the base directory or output file') cmlenz@162: palgarvio@172: if not options.output_file and not options.compile_all: cmlenz@162: options.output_file = os.path.join(options.directory, cmlenz@162: options.locale, 'LC_MESSAGES', cmlenz@162: options.domain + '.mo') palgarvio@172: if not options.compile_all: palgarvio@172: if not os.path.exists(os.path.dirname(options.output_file)): palgarvio@172: os.makedirs(os.path.dirname(options.output_file)) cmlenz@162: palgarvio@172: infile = open(options.input_file, 'r') palgarvio@172: try: palgarvio@172: catalog = read_po(infile) palgarvio@172: finally: palgarvio@172: infile.close() palgarvio@172: palgarvio@172: print 'compiling catalog to %r' % options.output_file palgarvio@172: palgarvio@172: outfile = open(options.output_file, 'w') palgarvio@172: try: palgarvio@172: write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) palgarvio@172: finally: palgarvio@172: outfile.close() palgarvio@172: else: palgarvio@172: for po in available_pos: palgarvio@172: infile = open(po, 'r') palgarvio@172: try: palgarvio@172: catalog = read_po(infile) palgarvio@172: finally: palgarvio@172: infile.close() cmlenz@162: palgarvio@172: print 'compiling catalog to %r' % po.replace('.po', '.mo') cmlenz@162: palgarvio@172: outfile = open(po.replace('.po', '.mo'), 'w') palgarvio@172: try: palgarvio@172: write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) palgarvio@172: finally: palgarvio@172: outfile.close() palgarvio@172: cmlenz@162: cmlenz@54: def extract(self, argv): cmlenz@54: """Subcommand for extracting messages from source files and generating cmlenz@54: a POT file. cmlenz@54: cmlenz@54: :param argv: the command arguments cmlenz@54: """ palgarvio@68: parser = OptionParser(usage=self.usage % ('extract', 'dir1 ...'), cmlenz@163: description=self.commands['extract']) cmlenz@54: parser.add_option('--charset', dest='charset', cmlenz@162: help='charset to use in the output (default ' cmlenz@162: '"%default")') cmlenz@54: parser.add_option('-k', '--keyword', dest='keywords', action='append', cmlenz@54: help='keywords to look for in addition to the ' cmlenz@54: 'defaults. You can specify multiple -k flags on ' cmlenz@54: 'the command line.') cmlenz@54: parser.add_option('--no-default-keywords', dest='no_default_keywords', cmlenz@54: action='store_true', cmlenz@54: help="do not include the default keywords") cmlenz@54: parser.add_option('--mapping', '-F', dest='mapping_file', cmlenz@54: help='path to the extraction mapping file') cmlenz@54: parser.add_option('--no-location', dest='no_location', cmlenz@54: action='store_true', cmlenz@54: help='do not include location comments with filename ' cmlenz@54: 'and line number') cmlenz@54: parser.add_option('--omit-header', dest='omit_header', cmlenz@54: action='store_true', cmlenz@54: help='do not include msgid "" entry in header') cmlenz@54: parser.add_option('-o', '--output', dest='output', cmlenz@54: help='path to the output POT file') cmlenz@54: parser.add_option('-w', '--width', dest='width', type='int', cmlenz@54: help="set output line width (default %default)") cmlenz@54: parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', cmlenz@54: help='do not break long message lines, longer than ' cmlenz@54: 'the output line width, into several lines') palgarvio@73: parser.add_option('--sort-output', dest='sort_output', palgarvio@73: action='store_true', palgarvio@73: help='generate sorted output (default False)') palgarvio@73: parser.add_option('--sort-by-file', dest='sort_by_file', palgarvio@73: action='store_true', palgarvio@73: help='sort output by file location (default False)') palgarvio@80: parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', palgarvio@80: metavar='EMAIL@ADDRESS', palgarvio@80: help='set report address for msgid') palgarvio@81: parser.add_option('--copyright-holder', dest='copyright_holder', palgarvio@81: help='set copyright holder in output') cmlenz@97: parser.add_option('--add-comments', '-c', dest='comment_tags', palgarvio@82: metavar='TAG', action='append', palgarvio@82: help='place comment block with TAG (or those ' palgarvio@82: 'preceding keyword lines) in output file. One ' palgarvio@82: 'TAG per argument call') cmlenz@54: cmlenz@54: parser.set_defaults(charset='utf-8', keywords=[], cmlenz@54: no_default_keywords=False, no_location=False, palgarvio@73: omit_header = False, width=76, no_wrap=False, palgarvio@82: sort_output=False, sort_by_file=False, cmlenz@97: comment_tags=[]) cmlenz@54: options, args = parser.parse_args(argv) cmlenz@54: if not args: cmlenz@54: parser.error('incorrect number of arguments') cmlenz@54: cmlenz@54: if options.output not in (None, '-'): cmlenz@54: outfile = open(options.output, 'w') cmlenz@54: else: cmlenz@54: outfile = sys.stdout cmlenz@54: cmlenz@54: keywords = DEFAULT_KEYWORDS.copy() cmlenz@54: if options.no_default_keywords: cmlenz@54: if not options.keywords: cmlenz@54: parser.error('you must specify new keywords if you disable the ' cmlenz@54: 'default ones') cmlenz@54: keywords = {} cmlenz@54: if options.keywords: cmlenz@54: keywords.update(parse_keywords(options.keywords)) cmlenz@54: cmlenz@54: if options.mapping_file: cmlenz@54: fileobj = open(options.mapping_file, 'U') cmlenz@54: try: cmlenz@54: method_map, options_map = parse_mapping(fileobj) cmlenz@54: finally: cmlenz@54: fileobj.close() cmlenz@54: else: cmlenz@54: method_map = DEFAULT_MAPPING cmlenz@54: options_map = {} cmlenz@54: cmlenz@54: if options.width and options.no_wrap: cmlenz@54: parser.error("'--no-wrap' and '--width' are mutually exclusive.") cmlenz@54: elif not options.width and not options.no_wrap: cmlenz@54: options.width = 76 cmlenz@54: elif not options.width and options.no_wrap: cmlenz@54: options.width = 0 palgarvio@89: palgarvio@73: if options.sort_output and options.sort_by_file: palgarvio@73: parser.error("'--sort-output' and '--sort-by-file' are mutually " palgarvio@73: "exclusive") cmlenz@54: cmlenz@54: try: cmlenz@104: catalog = Catalog(msgid_bugs_address=options.msgid_bugs_address, palgarvio@107: copyright_holder=options.copyright_holder, cmlenz@104: charset=options.charset) cmlenz@104: cmlenz@54: for dirname in args: cmlenz@54: if not os.path.isdir(dirname): cmlenz@54: parser.error('%r is not a directory' % dirname) cmlenz@97: extracted = extract_from_dir(dirname, method_map, options_map, cmlenz@97: keywords, options.comment_tags) palgarvio@82: for filename, lineno, message, comments in extracted: cmlenz@54: filepath = os.path.normpath(os.path.join(dirname, filename)) palgarvio@82: catalog.add(message, None, [(filepath, lineno)], pjenvey@112: auto_comments=comments) cmlenz@58: cmlenz@106: write_po(outfile, catalog, width=options.width, cmlenz@106: no_location=options.no_location, cmlenz@106: omit_header=options.omit_header, cmlenz@106: sort_output=options.sort_output, pjenvey@114: sort_by_file=options.sort_by_file) cmlenz@54: finally: cmlenz@54: if options.output: cmlenz@54: outfile.close() cmlenz@54: cmlenz@54: def init(self, argv): cmlenz@54: """Subcommand for creating new message catalogs from a template. cmlenz@167: cmlenz@54: :param argv: the command arguments cmlenz@54: """ cmlenz@167: parser = OptionParser(usage=self.usage % ('init', ''), cmlenz@163: description=self.commands['init']) palgarvio@68: parser.add_option('--domain', '-D', dest='domain', cmlenz@106: help="domain of PO file (default '%default')") palgarvio@68: parser.add_option('--input-file', '-i', dest='input_file', cmlenz@106: metavar='FILE', help='name of the input file') palgarvio@68: parser.add_option('--output-dir', '-d', dest='output_dir', cmlenz@106: metavar='DIR', help='path to output directory') palgarvio@68: parser.add_option('--output-file', '-o', dest='output_file', cmlenz@106: metavar='FILE', palgarvio@68: help="name of the output file (default " palgarvio@89: "'//LC_MESSAGES/" palgarvio@89: ".po')") cmlenz@106: parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', palgarvio@68: help='locale for the new localized catalog') palgarvio@89: cmlenz@106: parser.set_defaults(domain='messages') palgarvio@68: options, args = parser.parse_args(argv) palgarvio@89: cmlenz@106: if not options.locale: cmlenz@106: parser.error('you must provide a locale for the new catalog') cmlenz@106: try: cmlenz@106: locale = Locale.parse(options.locale) cmlenz@106: except UnknownLocaleError, e: cmlenz@106: parser.error(e) palgarvio@89: palgarvio@68: if not options.input_file: palgarvio@68: parser.error('you must specify the input file') palgarvio@89: cmlenz@106: if not options.output_file and not options.output_dir: cmlenz@106: parser.error('you must specify the output file or directory') palgarvio@89: cmlenz@106: if not options.output_file: palgarvio@68: options.output_file = os.path.join(options.output_dir, cmlenz@106: options.locale, 'LC_MESSAGES', palgarvio@68: options.domain + '.po') palgarvio@89: if not os.path.exists(os.path.dirname(options.output_file)): palgarvio@89: os.makedirs(os.path.dirname(options.output_file)) palgarvio@89: palgarvio@68: infile = open(options.input_file, 'r') cmlenz@106: try: cmlenz@106: catalog = read_po(infile) cmlenz@106: finally: cmlenz@106: infile.close() palgarvio@89: cmlenz@106: catalog.locale = locale cmlenz@134: catalog.revision_date = datetime.now(LOCALTZ) palgarvio@68: cmlenz@92: print 'creating catalog %r based on %r' % (options.output_file, cmlenz@92: options.input_file) palgarvio@68: cmlenz@106: outfile = open(options.output_file, 'w') cmlenz@106: try: cmlenz@106: write_po(outfile, catalog) cmlenz@106: finally: cmlenz@106: outfile.close() cmlenz@54: cmlenz@167: cmlenz@54: def main(): cmlenz@54: CommandLineInterface().run(sys.argv) cmlenz@3: cmlenz@50: def parse_mapping(fileobj, filename=None): cmlenz@49: """Parse an extraction method mapping from a file-like object. cmlenz@54: cmlenz@49: >>> buf = StringIO(''' cmlenz@49: ... # Python source files cmlenz@64: ... [python: **.py] cmlenz@54: ... cmlenz@49: ... # Genshi templates cmlenz@64: ... [genshi: **/templates/**.html] cmlenz@50: ... include_attrs = cmlenz@64: ... [genshi: **/templates/**.txt] cmlenz@146: ... template_class = genshi.template:TextTemplate cmlenz@50: ... encoding = latin-1 cmlenz@49: ... ''') cmlenz@54: cmlenz@49: >>> method_map, options_map = parse_mapping(buf) cmlenz@54: cmlenz@64: >>> method_map[0] cmlenz@64: ('**.py', 'python') cmlenz@64: >>> options_map['**.py'] cmlenz@49: {} cmlenz@64: >>> method_map[1] cmlenz@64: ('**/templates/**.html', 'genshi') cmlenz@64: >>> options_map['**/templates/**.html']['include_attrs'] cmlenz@49: '' cmlenz@64: >>> method_map[2] cmlenz@64: ('**/templates/**.txt', 'genshi') cmlenz@64: >>> options_map['**/templates/**.txt']['template_class'] cmlenz@146: 'genshi.template:TextTemplate' cmlenz@64: >>> options_map['**/templates/**.txt']['encoding'] cmlenz@49: 'latin-1' cmlenz@54: cmlenz@49: :param fileobj: a readable file-like object containing the configuration cmlenz@49: text to parse cmlenz@49: :return: a `(method_map, options_map)` tuple cmlenz@49: :rtype: `tuple` cmlenz@49: :see: `extract_from_directory` cmlenz@49: """ cmlenz@64: method_map = [] cmlenz@49: options_map = {} cmlenz@49: cmlenz@50: parser = RawConfigParser() cmlenz@64: parser._sections = odict(parser._sections) # We need ordered sections cmlenz@50: parser.readfp(fileobj, filename) cmlenz@50: for section in parser.sections(): cmlenz@50: method, pattern = [part.strip() for part in section.split(':', 1)] cmlenz@64: method_map.append((pattern, method)) cmlenz@50: options_map[pattern] = dict(parser.items(section)) cmlenz@49: cmlenz@49: return (method_map, options_map) cmlenz@49: cmlenz@14: def parse_keywords(strings=[]): cmlenz@14: """Parse keywords specifications from the given list of strings. cmlenz@54: cmlenz@14: >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']) cmlenz@14: >>> for keyword, indices in sorted(kw.items()): cmlenz@14: ... print (keyword, indices) cmlenz@14: ('_', None) cmlenz@14: ('dgettext', (2,)) cmlenz@14: ('dngettext', (2, 3)) cmlenz@14: """ cmlenz@14: keywords = {} cmlenz@14: for string in strings: cmlenz@14: if ':' in string: cmlenz@14: funcname, indices = string.split(':') cmlenz@14: else: cmlenz@14: funcname, indices = string, None cmlenz@14: if funcname not in keywords: cmlenz@14: if indices: cmlenz@14: indices = tuple([(int(x)) for x in indices.split(',')]) cmlenz@14: keywords[funcname] = indices cmlenz@14: return keywords cmlenz@14: cmlenz@54: cmlenz@3: if __name__ == '__main__': palgarvio@57: main()