changeset 51:7f61453c1bea

Fixed a bug regarding plural msgid's handling when writing the `.pot` file. Renamed old `write_po` to `write_pot` which is what it actually does and also adds space to the new `write_po`. Changed tests accordingly. Added support to create new localized catalogs from a catalog template, `write_po`..
author palgarvio
date Thu, 07 Jun 2007 22:48:47 +0000
parents fa8a27b80eb4
children 1e724c305460
files babel/catalog/frontend.py babel/catalog/plurals.py babel/catalog/pofile.py babel/catalog/tests/pofile.py doc/setup.txt setup.py
diffstat 6 files changed, 423 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/babel/catalog/frontend.py
+++ b/babel/catalog/frontend.py
@@ -24,9 +24,12 @@
 import sys
 
 from babel import __version__ as VERSION
+from babel import Locale
+from babel.core import UnknownLocaleError
 from babel.catalog.extract import extract_from_dir, DEFAULT_KEYWORDS, \
                                   DEFAULT_MAPPING
-from babel.catalog.pofile import write_po
+from babel.catalog.pofile import write_po, write_pot
+from babel.catalog.plurals import PLURALS
 
 __all__ = ['extract_messages', 'check_message_extractors', 'main']
 __docformat__ = 'restructuredtext en'
@@ -147,31 +150,15 @@
                 filepath = os.path.normpath(filename)
                 messages.append((filepath, lineno, funcname, message, None))
 
-            log.info('writing PO file to %s' % self.output_file)
-            write_po(outfile, messages, project=self.distribution.get_name(),
+            log.info('writing PO template file to %s' % self.output_file)
+            write_pot(outfile, messages, project=self.distribution.get_name(),
                      version=self.distribution.get_version(), width=self.width,
                      charset=self.charset, no_location=self.no_location,
                      omit_header=self.omit_header)
         finally:
             outfile.close()
 
-def check_message_extractors(dist, name, value):
-    """Validate the ``message_extractors`` keyword argument to ``setup()``.
-    
-    :param dist: the distutils/setuptools ``Distribution`` object
-    :param name: the name of the keyword argument (should always be
-                 "message_extractors")
-    :param value: the value of the keyword argument
-    :raise `DistutilsSetupError`: if the value is not valid
-    :see: `Adding setup() arguments
-           <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_
-    """
-    assert name == 'message_extractors'
-    if not isinstance(value, (basestring, dict)):
-        raise DistutilsSetupError('the value of the "extract_messages" '
-                                  'parameter must be a string or dictionary')
-
-def main(argv=sys.argv):
+def extract_cmdline(argv=sys.argv):
     """Command-line interface.
     
     This function provides a simple command-line interface to the message
@@ -259,6 +246,135 @@
     finally:
         if options.output:
             outfile.close()
+            
+class new_catalog(Command):
+    """New catalog command for use in ``setup.py`` scripts.
+
+    If correctly installed, this command is available to Setuptools-using
+    setup scripts automatically. For projects using plain old ``distutils``,
+    the command needs to be registered explicitly in ``setup.py``::
+
+        from babel.catalog.frontend import new_catalog
+
+        setup(
+            ...
+            cmdclass = {'new_catalog': new_catalog}
+        )
+
+    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+    """
+
+    description = 'create new catalogs based on a catalog template'
+    user_options = [
+        ('input-file=', 'i',
+         'name of the input file'),
+        ('output-dir=', 'd',
+         'path to output directory'),
+        ('output-file=', 'o',
+         "name of the output file (default "
+         "'<output_dir>/<locale>.po')"),
+        ('locale=', 'l',
+         'locale for the new localized catalog'),
+        ('first-author=', None,
+         'name of first author'),
+        ('first-author-email=', None,
+         'email of first author')
+    ]
+
+    def initialize_options(self):
+        self.output_dir = None
+        self.output_file = None
+        self.input_file = None
+        self.locale = None
+        self.first_author = None
+        self.first_author_email = None
+
+    def finalize_options(self):
+        if not self.input_file:
+            raise DistutilsOptionError('you must specify the input file')
+        
+        if not self.locale:
+            raise DistutilsOptionError('you must provide a locale for the '
+                                       'new catalog')
+        else:
+            try:
+                locale = Locale.parse(self.locale)
+            except UnknownLocaleError, error:
+                log.error(error)
+                sys.exit(1)
+                
+        self._locale_parts = self.locale.split('_')
+        self._language = None
+        self._country = None
+        _locale = Locale('en')
+        if len(self._locale_parts) == 2:
+            if self._locale_parts[0] == self._locale_parts[1].lower():
+                # Remove country part if equal to language
+                locale = self._locale_parts[0]
+            else:
+                locale = self.locale
+            self._language = _locale.languages[self._locale_parts[0]]
+            self._country = _locale.territories[self._locale_parts[1]]
+        else:
+            locale = self._locale_parts[0]
+            self._language = _locale.languages[locale]
+            
+        if not self.output_file and not self.output_dir:
+            raise DistutilsOptionError('you must specify the output directory')
+        
+        if not self.output_file and self.output_dir:
+            self.output_file = os.path.join(self.output_dir, locale + '.po')
+
+
+    def run(self):
+        outfile = open(self.output_file, 'w')
+        infile = open(self.input_file, 'r')
+        
+        if PLURALS.has_key(self.locale):
+            # Try <language>_<COUNTRY>
+            plurals = PLURALS[self.locale]
+        elif PLURALS.has_key(self._locale_parts[0]):
+            # Try <language>
+            plurals = PLURALS[self._locale_parts[0]]
+        else:
+            plurals = ('INTEGER', 'EXPRESSION')
+            
+        if self._country:
+            logline = 'Creating %%s (%s) %%r PO from %%r' % self._country + \
+                      ' PO template' 
+        else:
+            logline = 'Creating %s %r PO from %r PO template'
+        log.info(logline, self._language, self.output_file, self.input_file)
+        
+        write_po(outfile, infile, self._language, country=self._country,
+                 project=self.distribution.get_name(),
+                 version=self.distribution.get_version(),
+                 charset=self.charset, plurals=plurals,
+                 first_author=self.first_author,
+                 first_author_email=self.first_author_email)
+        infile.close()
+        outfile.close()
+
+            
+def new_catalog_cmdline(argv=sys.argv):
+    pass
+
+def check_message_extractors(dist, name, value):
+    """Validate the ``message_extractors`` keyword argument to ``setup()``.
+    
+    :param dist: the distutils/setuptools ``Distribution`` object
+    :param name: the name of the keyword argument (should always be
+                 "message_extractors")
+    :param value: the value of the keyword argument
+    :raise `DistutilsSetupError`: if the value is not valid
+    :see: `Adding setup() arguments
+           <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_
+    """
+    assert name == 'message_extractors'
+    if not isinstance(value, (basestring, dict)):
+        raise DistutilsSetupError('the value of the "extract_messages" '
+                                  'parameter must be a string or dictionary')
 
 def parse_mapping(fileobj, filename=None):
     """Parse an extraction method mapping from a file-like object.
@@ -333,4 +449,4 @@
     return keywords
 
 if __name__ == '__main__':
-    main()
+    extract_cmdline()
new file mode 100644
--- /dev/null
+++ b/babel/catalog/plurals.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+PLURALS = {
+    # Afrikaans - From Pootle's PO's
+    'af': (2, '(n != 1)'),
+    # Arabic - From Pootle's PO's
+    'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'),
+    # Bulgarian - From Pootle's PO's
+    'bg': (2, '(n != 1)'),
+    # Bengali - From Pootle's PO's
+    'bn': (2, '(n != 1)'),
+    # Catalan - From Pootle's PO's
+    'ca': (2, '(n != 1)'),
+    # Czech
+    'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Danish
+    'da': (2, '(n != 1)'),
+    # German
+    'de': (2, '(n != 1)'),
+    # Greek
+    'el': (2, '(n != 1)'),
+    # English
+    'en': (2, '(n != 1)'),
+    # Esperanto
+    'eo': (2, '(n != 1)'),
+    # Spanish
+    'es': (2, '(n != 1)'),
+    # Estonian
+    'et': (2, '(n != 1)'),
+    # Basque - From Pootle's PO's
+    'eu': (2, '(n != 1)'),
+    # Persian - From Pootle's PO's
+    'fa': (1, '0'),
+    # Finnish
+    'fi': (2, '(n != 1)'),
+    # French
+    'fr': (2, '(n > 1)'),
+    # Furlan - From Pootle's PO's
+    'fur': (2, '(n > 1)'),
+    # Irish
+    'ga': (3, 'n==1 ? 0 : n==2 ? 1 : 2'),
+    # Galego - From Pootle's PO's
+    'gl': (2, '(n != 1)'),
+    # Hausa - From Pootle's PO's
+    'ha': (2, '(n != 1)'), 
+    # Hebrew
+    'he': (2, '(n != 1)'),
+    # Hindi - From Pootle's PO's
+    'hi': (2, '(n != 1)'), 
+    # Croatian
+    'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Hungarian
+    'hu': (1, '0'),
+    # Armenian - From Pootle's PO's
+    'hy': (1, '0'),
+    # Icelandic - From Pootle's PO's
+    'is': (2, '(n != 1)'),
+    # Italian
+    'it': (2, '(n != 1)'),
+    # Japanese
+    'ja': (1, '0'),
+    # Georgian - From Pootle's PO's
+    'ka': (1, '0'),
+    # Kongo - From Pootle's PO's
+    'kg': (2, '(n != 1)'),
+    # Khmer - From Pootle's PO's
+    'km': (1, '0'),
+    # Korean
+    'ko': (1, '0'),
+    # KurdĂ® - From Pootle's PO's
+    'ku': (2, '(n != 1)'),
+    # Lithuanian
+    'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Latvian
+    'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'),
+    # Maltese - From Pootle's PO's
+    'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'),
+    # Norwegian Bokmal
+    'nb': (2, '(n != 1)'),
+    # Dutch
+    'nl': (2, '(n != 1)'),
+    # Norwegian Nynorsk
+    'nn': (2, '(n != 1)'),
+    # Norwegian
+    'no': (2, '(n != 1)'),
+    # Punjabi - From Pootle's PO's
+    'pa': (2, '(n != 1)'),
+    # Polish
+    'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Portuguese
+    'pt': (2, '(n != 1)'),
+    # Brazilian
+    'pt_BR': (2, '(n > 1)'),
+    # Romanian - From Pootle's PO's
+    'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'),
+    # Russian
+    'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Slovak
+    'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Slovenian
+    'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'),
+    # Serbian - From Pootle's PO's
+    'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Sesotho - From Pootle's PO's
+    'st': (2, '(n != 1)'),
+    # Swedish
+    'sv': (2, '(n != 1)'),
+    # Turkish
+    'tr': (1, '0'),
+    # Ukrainian
+    'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Venda - From Pootle's PO's
+    've': (2, '(n != 1)'),
+    # Vietnamese - From Pootle's PO's
+    'vi': (1, '0'),
+    # Xhosa - From Pootle's PO's
+    'xh': (2, '(n != 1)'),
+    # Chinese - From Pootle's PO's
+    'zh_CN': (1, '0'),
+    'zh_HK': (1, '0'),
+    'zh_TW': (1, '0'),
+}
--- a/babel/catalog/pofile.py
+++ b/babel/catalog/pofile.py
@@ -29,7 +29,7 @@
 
 from babel import __version__ as VERSION
 
-__all__ = ['escape', 'normalize', 'read_po', 'write_po']
+__all__ = ['escape', 'normalize', 'read_po', 'write_po', 'write_pot']
 
 def read_po(fileobj):
     """Read messages from a ``gettext`` PO (portable object) file from the given
@@ -238,13 +238,12 @@
     if not lines[-1]:
         del lines[-1]
         lines[-1] += '\n'
-
     return u'""\n' + u'\n'.join([escape(l) for l in lines])
 
-def write_po(fileobj, messages, project='PROJECT', version='VERSION', width=76,
+def write_pot(fileobj, messages, project='PROJECT', version='VERSION', width=76,
              charset='utf-8', no_location=False, omit_header=False):
-    r"""Write a ``gettext`` PO (portable object) file to the given file-like
-    object.
+    r"""Write a ``gettext`` PO (portable object) template file to the given
+    file-like object.
     
     The `messages` parameter is expected to be an iterable object producing
     tuples of the form:
@@ -253,7 +252,7 @@
     
     >>> from StringIO import StringIO
     >>> buf = StringIO()
-    >>> write_po(buf, [
+    >>> write_pot(buf, [
     ...     ('main.py', 1, None, u'foo %(name)s', ('fuzzy',)),
     ...     ('main.py', 3, 'ngettext', (u'bar', u'baz'), None)
     ... ], omit_header=True)
@@ -303,19 +302,23 @@
     locations = {}
     msgflags = {}
     msgids = []
+    plurals = {}
 
     for filename, lineno, funcname, key, flags in messages:
         flags = set(flags or [])
+        if isinstance(key, (list, tuple)):
+            assert len(key) == 2
+            plurals[key[0]] = key[1]
+            key = key[0]
         if key in msgids:
             locations[key].append((filename, lineno))
             msgflags[key] |= flags
         else:
-            if (isinstance(key, (list, tuple)) and
-                    filter(None, [PYTHON_FORMAT(k) for k in key])) or \
-                    (isinstance(key, basestring) and PYTHON_FORMAT(key)):
+            if PYTHON_FORMAT(key):
                 flags.add('python-format')
             else:
-                flags.discard('python-format')
+                flags.discard('python-format')              
+        
             locations[key] = [(filename, lineno)]
             msgflags[key] = flags
             msgids.append(key)
@@ -331,13 +334,139 @@
         if flags:
             _write('#%s\n' % ', '.join([''] + list(flags)))
 
-        if type(msgid) is tuple:
-            assert len(msgid) == 2
-            _write('msgid %s\n' % _normalize(msgid[0]))
-            _write('msgid_plural %s\n' % _normalize(msgid[1]))
+        if plurals.has_key(msgid):
+            _write('msgid %s\n' % _normalize(msgid))
+            _write('msgid_plural %s\n' % _normalize(plurals[msgid]))
             _write('msgstr[0] ""\n')
             _write('msgstr[1] ""\n')
         else:
             _write('msgid %s\n' % _normalize(msgid))
             _write('msgstr ""\n')
         _write('\n')
+
+def write_po(fileobj, input_fileobj, language, country=None, project='PROJECT',
+             version='VERSION', first_author=None, first_author_email=None,
+             plurals=('INTEGER', 'EXPRESSION')):
+    r"""Write a ``gettext`` PO (portable object) file to the given file-like 
+    object, from the given input PO template file.
+    
+    >>> from StringIO import StringIO
+    >>> inbuf = StringIO(r'''# Translations Template for FooBar.
+    ... # Copyright (C) 2007 ORGANIZATION
+    ... # This file is distributed under the same license as the
+    ... # FooBar project.
+    ... # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+    ... #
+    ... #, fuzzy
+    ... msgid ""
+    ... msgstr ""
+    ... "Project-Id-Version: FooBar 0.1\n"
+    ... "POT-Creation-Date: 2007-06-07 22:54+0100\n"
+    ... "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+    ... "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+    ... "Language-Team: LANGUAGE <LL@li.org>\n"
+    ... "MIME-Version: 1.0\n"
+    ... "Content-Type: text/plain; charset=utf-8\n"
+    ... "Content-Transfer-Encoding: 8bit\n"
+    ... "Generated-By: Babel 0.1dev-r50\n"
+    ...
+    ... #: base.py:83 templates/index.html:9
+    ... #: templates/index2.html:9
+    ... msgid "Home"
+    ... msgstr ""
+    ...
+    ... #: base.py:84 templates/index.html:9
+    ... msgid "Accounts"
+    ... msgstr ""
+    ... ''')
+    >>> outbuf = StringIO()
+    >>> write_po(outbuf, inbuf, 'English', project='FooBar',
+    ...          version='0.1', first_author='A Name', 
+    ...          first_author_email='user@domain.tld',
+    ...          plurals=(2, '(n != 1)'))
+    >>> print outbuf.getvalue() # doctest: +ELLIPSIS
+    # English Translations for FooBar
+    # Copyright (C) 2007 ORGANIZATION
+    # This file is distributed under the same license as the
+    # FooBar project.
+    # A Name <user@domain.tld>, ...
+    #
+    #, fuzzy
+    msgid ""
+    msgstr ""
+    "Project-Id-Version: FooBar 0.1\n"
+    "POT-Creation-Date: 2007-06-07 22:54+0100\n"
+    "PO-Revision-Date: ...\n"
+    "Last-Translator: A Name <user@domain.tld>\n"
+    "Language-Team: LANGUAGE <LL@li.org>\n"
+    "MIME-Version: 1.0\n"
+    "Content-Type: text/plain; charset=utf-8\n"
+    "Content-Transfer-Encoding: 8bit\n"
+    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
+    "Generated-By: Babel ...\n"
+    <BLANKLINE>
+    #: base.py:83 templates/index.html:9
+    #: templates/index2.html:9
+    msgid "Home"
+    msgstr ""
+    <BLANKLINE>
+    #: base.py:84 templates/index.html:9
+    msgid "Accounts"
+    msgstr ""
+    <BLANKLINE>
+    >>>
+    """
+    
+    _first_author = ''
+    if first_author:
+        _first_author += first_author
+    if first_author_email:
+        _first_author += ' <%s>' % first_author_email
+
+    inlines = input_fileobj.readlines()
+    outlines = []
+    in_header = True
+    for index in range(len(inlines)):
+        if in_header:
+            if '# Translations Template' in inlines[index]:
+                if country:
+                    line = '# %s (%s) Translations for %%s\n' % \
+                                                            (language, country)
+                else:
+                    line = '# %s Translations for %%s\n' % language
+                outlines.append(line % project)
+            elif '# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.' in inlines[index]:
+                if _first_author:
+                    outlines.append(
+                        '# %s, %s\n' % (_first_author, time.strftime('%Y'))
+                    )
+                else:
+                    outlines.append(inlines[index])
+            elif '"PO-Revision-Date:' in inlines[index]:
+                outlines.append(
+                    '"PO-Revision-Date: %s\\n"\n' % \
+                    time.strftime('%Y-%m-%d %H:%M%z')
+                )
+            elif '"Last-Translator:' in inlines[index]:
+                if _first_author:
+                    outlines.append(
+                        '"Last-Translator: %s\\n"\n' % _first_author
+                    )
+                else:
+                    outlines.append(inlines[index])
+            elif '"Content-Transfer-Encoding:' in inlines[index]:
+                outlines.append(inlines[index])
+                if '"Plural-Forms:' not in inlines[index+1]:
+                    outlines.append(
+                        '"Plural-Forms: nplurals=%s; plural=%s;\\n"\n' % plurals
+                    )
+            elif inlines[index].endswith('\\n"\n') and \
+                 inlines[index+1] == '\n':
+                in_header = False
+                outlines.append(inlines[index])
+            else:
+                outlines.append(inlines[index])
+        else:
+            outlines.extend(inlines[index:])
+            break
+    fileobj.writelines(outlines)
--- a/babel/catalog/tests/pofile.py
+++ b/babel/catalog/tests/pofile.py
@@ -26,11 +26,11 @@
         assert pofile.PYTHON_FORMAT('foo %r bar')
 
 
-class WritePoTestCase(unittest.TestCase):
+class WritePotTestCase(unittest.TestCase):
 
     def test_join_locations(self):
         buf = StringIO()
-        pofile.write_po(buf, [
+        pofile.write_pot(buf, [
             ('main.py', 1, None, u'foo', None),
             ('utils.py', 3, None, u'foo', None),
         ], omit_header=True)
@@ -46,7 +46,7 @@
 
 """
         buf = StringIO()
-        pofile.write_po(buf, [
+        pofile.write_pot(buf, [
             ('main.py', 1, None, text, None),
         ], no_location=True, omit_header=True, width=42)
         self.assertEqual(r'''msgid ""
@@ -63,7 +63,7 @@
 includesareallylongwordthatmightbutshouldnt throw us into an infinite loop
 """
         buf = StringIO()
-        pofile.write_po(buf, [
+        pofile.write_pot(buf, [
             ('main.py', 1, None, text, None),
         ], no_location=True, omit_header=True, width=32)
         self.assertEqual(r'''msgid ""
@@ -78,7 +78,7 @@
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(pofile))
     suite.addTest(unittest.makeSuite(PythonFormatFlagTestCase))
-    suite.addTest(unittest.makeSuite(WritePoTestCase))
+    suite.addTest(unittest.makeSuite(WritePotTestCase))
     return suite
 
 if __name__ == '__main__':
--- a/doc/setup.txt
+++ b/doc/setup.txt
@@ -59,14 +59,14 @@
       --no-wrap              do not break long message lines, longer than the
                              output line width, into several lines
 
-Running the command will produce a PO file::
+Running the command will produce a PO template file::
 
     $ ./setup.py extract_messages --output-file foobar/locale/messages.pot
     running extract_messages
     extracting messages from foobar/__init__.py
     extracting messages from foobar/core.py
     ...
-    writing PO file to foobar/locale/messages.pot
+    writing PO template file to foobar/locale/messages.pot
 
 
 Method Mapping
--- a/setup.py
+++ b/setup.py
@@ -127,10 +127,12 @@
 
     entry_points = """
     [console_scripts]
-    pygettext = babel.catalog.frontend:main
+    pygettext = babel.catalog.frontend:extract_cmdline
+    pymsginit = babel.catalog.frontend:new_catalog_cmdline
     
     [distutils.commands]
     extract_messages = babel.catalog.frontend:extract_messages
+    new_catalog = babel.catalog.frontend:new_catalog
     
     [distutils.setup_keywords]
     message_extractors = babel.catalog.frontend:check_message_extractors
Copyright (C) 2012-2017 Edgewall Software