Commits

Anonymous committed 7870274

Import of initial code base.

Comments (0)

Files changed (432)

+Copyright (C) 2007 Edgewall Software
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+ 3. The name of the author may not be used to endorse or promote
+    products derived from this software without specific prior
+    written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Nothing so far, not even public yet.
+Installing Babel
+================
+
+Prerequisites
+-------------
+
+ * Python 2.3 or later (2.4 or later is recommended)
+ * Optional: setuptools 0.6b1 or later
+
+
+Installation
+------------
+
+Once you've downloaded and unpacked a Babel source release, enter the
+directory where the archive was unpacked, and run:
+
+  $ python setup.py install
+
+Note that you may need administrator/root privileges for this step, as
+this command will by default attempt to install Babel to the Python
+site-packages directory on your system.
+
+For advanced options, please refer to the easy_install and/or the distutils
+documentation:
+
+  http://peak.telecommunity.com/DevCenter/EasyInstall
+  http://docs.python.org/inst/inst.html
+
+
+Support
+-------
+
+If you encounter any problems with Babel, please don't hesitate to ask
+questions on the Babel mailing list or IRC channel:
+
+  http://babel.edgewall.org/wiki/MailingList
+  http://babel.edgewall.org/wiki/IrcChannel
+include babel/localedata/*.dat
+include doc/api/*.*
+include doc/*.html
+About Babel
+===========
+
+Babel is a Python library that provides an integrated collection of
+utilities that assist with internationalizing and localizing Python
+applications (in particular web-based applications.)
+
+Details can be found in the HTML files in the `doc` folder.
+
+For more information please visit the Babel web site:
+
+  <http://babel.edgewall.org/>
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+"""Integrated collection of utilities that assist in internationalizing and
+localizing applications.
+
+This package is basically composed of two major parts:
+
+ * tools to build and work with ``gettext`` message catalogs
+ * a Python interface to the CLDR (Common Locale Data Repository), providing
+   access to various locale display names, localized number and date
+   formatting, etc.
+
+:see: http://www.gnu.org/software/gettext/
+:see: http://docs.python.org/lib/module-gettext.html
+:see: http://www.unicode.org/cldr/
+"""
+
+from babel.core import Locale
+
+__docformat__ = 'restructuredtext en'
+__version__ = __import__('pkg_resources').get_distribution('Babel').version

babel/catalog/__init__.py

+# -*- 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/.
+
+"""Support for ``gettext`` message catalogs."""
+
+import gettext
+
+__all__ = ['Translations']
+
+DEFAULT_DOMAIN = 'messages'
+
+
+class Translations(gettext.GNUTranslations):
+    """An extended translation catalog class."""
+
+    def __init__(self, fileobj=None):
+        """Initialize the translations catalog.
+        
+        :param fileobj: the file-like object the translation should be read
+                        from
+        """
+        GNUTranslations.__init__(self, fp=fileobj)
+        self.files = [getattr(fileobj, 'name')]
+
+    def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
+        """Load translations from the given directory.
+        
+        :param dirname: the directory containing the ``MO`` files
+        :param locales: the list of locales in order of preference (items in
+                        this list can be either `Locale` objects or locale
+                        strings)
+        :param domain: the message domain
+        :return: the loaded catalog, or a ``NullTranslations`` instance if no
+                 matching translations were found
+        :rtype: `Translations`
+        """
+        locales = [str(locale) for locale in locales]
+        filename = gettext.find(domain, dirname, locales)
+        if not filename:
+            return NullTranslations()
+        return cls(open(filename, 'rb'))
+    load = classmethod(load)
+
+    def merge(self, translations):
+        """Merge the given translations into the catalog.
+        
+        Message translations in the specfied catalog override any messages with
+        the same identifier in the existing catalog.
+        
+        :param translations: the `Translations` instance with the messages to
+                             merge
+        :return: the `Translations` instance (``self``) so that `merge` calls
+                 can be easily chained
+        :rtype: `Translations`
+        """
+        if isinstance(translations, Translations):
+            self._catalog.update(translations._catalog)
+            self.files.extend(translations.files)
+        return self
+
+    def __repr__(self):
+        return "<%s %r>" % (type(self).__name__)

babel/catalog/extract.py

+# -*- 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/.
+
+"""Basic infrastructure for extracting localizable messages from source files.
+
+This module defines an extensible system for collecting localizable message
+strings from a variety of sources. A native extractor for Python source files
+is builtin, extractors for other sources can be added using very simple plugins.
+
+The main entry points into the extraction functionality are the functions
+`extract_from_dir` and `extract_from_file`.
+"""
+
+import os
+from pkg_resources import working_set
+import sys
+from tokenize import generate_tokens, NAME, OP, STRING
+
+from babel.util import extended_glob
+
+__all__ = ['extract', 'extract_from_dir', 'extract_from_file']
+__docformat__ = 'restructuredtext en'
+
+GROUP_NAME = 'babel.extractors'
+
+KEYWORDS = (
+    '_', 'gettext', 'ngettext',
+    'dgettext', 'dngettext',
+    'ugettext', 'ungettext'
+)
+
+DEFAULT_MAPPING = {
+    'genshi': ['*.html', '**/*.html'],
+    'python': ['*.py', '**/*.py']
+}
+
+def extract_from_dir(dirname, mapping=DEFAULT_MAPPING, keywords=KEYWORDS,
+                     options=None):
+    """Extract messages from any source files found in the given directory.
+    
+    This function generates tuples of the form:
+    
+        ``(filename, lineno, funcname, message)``
+    
+    Which extraction method used is per file is determined by the `mapping`
+    parameter, which maps extraction method names to lists of extended glob
+    patterns. For example, the following is the default mapping:
+    
+    >>> mapping = {
+    ...     'python': ['*.py', '**/*.py']
+    ... }
+    
+    This basically says that files with the filename extension ".py" at any
+    level inside the directory should be processed by the "python" extraction
+    method. Files that don't match any of the patterns are ignored.
+    
+    The following extended mapping would also use the "genshi" extraction method
+    on any file in "templates" subdirectory:
+    
+    >>> mapping = {
+    ...     'genshi': ['**/templates/*.*', '**/templates/**/*.*'],
+    ...     'python': ['*.py', '**/*.py']
+    ... }
+    
+    :param dirname: the path to the directory to extract messages from
+    :param mapping: a mapping of extraction method names to extended glob
+                    patterns
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(filename, lineno, funcname, message)`` tuples
+    :rtype: ``iterator``
+    """
+    extracted_files = {}
+    for method, patterns in mapping.items():
+        for pattern in patterns:
+            for filename in extended_glob(pattern, dirname):
+                if filename in extracted_files:
+                    continue
+                filepath = os.path.join(dirname, filename)
+                for line, func, key in extract_from_file(method, filepath,
+                                                         keywords=keywords,
+                                                         options=options):
+                    yield filename, line, func, key
+                extracted_files[filename] = True
+
+def extract_from_file(method, filename, keywords=KEYWORDS, options=None):
+    """Extract messages from a specific file.
+    
+    This function returns a list of tuples of the form:
+    
+        ``(lineno, funcname, message)``
+    
+    :param filename: the path to the file to extract messages from
+    :param method: a string specifying the extraction method (.e.g. "python")
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param options: a dictionary of additional options (optional)
+    :return: the list of extracted messages
+    :rtype: `list`
+    """
+    fileobj = open(filename, 'U')
+    try:
+        return list(extract(method, fileobj, keywords, options=options))
+    finally:
+        fileobj.close()
+
+def extract(method, fileobj, keywords=KEYWORDS, options=None):
+    """Extract messages from the given file-like object using the specified
+    extraction method.
+    
+    This function returns a list of tuples of the form:
+    
+        ``(lineno, funcname, message)``
+    
+    The implementation dispatches the actual extraction to plugins, based on the
+    value of the ``method`` parameter.
+    
+    >>> source = '''# foo module
+    ... def run(argv):
+    ...    print _('Hello, world!')
+    ... '''
+
+    >>> from StringIO import StringIO
+    >>> for message in extract('python', StringIO(source)):
+    ...     print message
+    (3, '_', 'Hello, world!')
+    
+    :param method: a string specifying the extraction method (.e.g. "python")
+    :param fileobj: the file-like object the messages should be extracted from
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param options: a dictionary of additional options (optional)
+    :return: the list of extracted messages
+    :rtype: `list`
+    :raise ValueError: if the extraction method is not registered
+    """
+    for entry_point in working_set.iter_entry_points(GROUP_NAME, method):
+        func = entry_point.load(require=True)
+        return list(func(fileobj, keywords, options=options or {}))
+    raise ValueError('Unknown extraction method %r' % method)
+
+def extract_genshi(fileobj, keywords, options):
+    """Extract messages from Genshi templates.
+    
+    :param fileobj: the file-like object the messages should be extracted from
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(lineno, funcname, message)`` tuples
+    :rtype: ``iterator``
+    """
+    from genshi.filters.i18n import Translator
+    from genshi.template import MarkupTemplate
+    tmpl = MarkupTemplate(fileobj, filename=getattr(fileobj, 'name'))
+    translator = Translator(None)
+    for message in translator.extract(tmpl.stream, gettext_functions=keywords):
+        yield message
+
+def extract_python(fileobj, keywords, options):
+    """Extract messages from Python source code.
+    
+    :param fileobj: the file-like object the messages should be extracted from
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(lineno, funcname, message)`` tuples
+    :rtype: ``iterator``
+    """
+    funcname = None
+    lineno = None
+    buf = []
+    messages = []
+    in_args = False
+
+    tokens = generate_tokens(fileobj.readline)
+    for tok, value, (lineno, _), _, _ in tokens:
+        if funcname and tok == OP and value == '(':
+            in_args = True
+        elif funcname and in_args:
+            if tok == OP and value == ')':
+                in_args = False
+                if buf:
+                    messages.append(''.join(buf))
+                    del buf[:]
+                if filter(None, messages):
+                    if len(messages) > 1:
+                        messages = tuple(messages)
+                    else:
+                        messages = messages[0]
+                    yield lineno, funcname, messages
+                funcname = lineno = None
+                messages = []
+            elif tok == STRING:
+                if lineno is None:
+                    lineno = stup[0]
+                buf.append(value[1:-1])
+            elif tok == OP and value == ',':
+                messages.append(''.join(buf))
+                del buf[:]
+        elif funcname:
+            funcname = None
+        elif tok == NAME and value in keywords:
+            funcname = value

babel/catalog/frontend.py

+# -*- 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/.
+
+"""Frontends for the message extraction functionality."""
+
+from distutils import log
+from distutils.cmd import Command
+from optparse import OptionParser
+import os
+import sys
+
+from babel import __version__ as VERSION
+from babel.catalog.extract import extract_from_dir, KEYWORDS
+from babel.catalog.pofile import write_po
+
+__all__ = ['extract_messages', 'main']
+__docformat__ = 'restructuredtext en'
+
+
+class extract_messages(Command):
+    """Message extraction 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 extract_messages
+    
+        setup(
+            ...
+            cmdclass = {'extract_messages': extract_messages}
+        )
+
+    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+    """
+
+    description = 'extract localizable strings from the project code'
+    user_options = [
+        ('charset=', None,
+         'charset to use in the output file'),
+        ('keywords=', 'k',
+         'comma-separated list of keywords to look for in addition to the '
+         'defaults'),
+        ('no-location', None,
+         'do not include location comments with filename and line number'),
+        ('omit-header', None,
+         'do not include msgid "" entry in header'),
+        ('output-file=', None,
+         'name of the output file'),
+    ]
+    boolean_options = ['no-location', 'omit-header']
+
+    def initialize_options(self):
+        self.charset = 'utf-8'
+        self.keywords = KEYWORDS
+        self.no_location = False
+        self.omit_header = False
+        self.output_file = None
+        self.input_dirs = None
+
+    def finalize_options(self):
+        if not self.input_dirs:
+            self.input_dirs = dict.fromkeys([k.split('.',1)[0]
+                for k in self.distribution.packages
+            ]).keys()
+        if isinstance(self.keywords, basestring):
+            new_keywords = [k.strip() for k in self.keywords.split(',')]
+            self.keywords = list(KEYWORDS) + new_keywords
+
+    def run(self):
+        outfile = open(self.output_file, 'w')
+        try:
+            messages = []
+            for dirname in self.input_dirs:
+                log.info('extracting messages from %r' % dirname)
+                extracted = extract_from_dir(dirname, keywords=self.keywords)
+                for filename, lineno, funcname, message in extracted:
+                    messages.append((os.path.join(dirname, filename), lineno,
+                                     funcname, message))
+            write_po(outfile, messages, charset=self.charset,
+                     no_location=self.no_location, omit_header=self.omit_header)
+            log.info('writing PO file to %s' % self.output_file)
+        finally:
+            outfile.close()
+
+
+def main(argv=sys.argv):
+    """Command-line interface.
+    
+    This function provides a simple command-line interface to the message
+    extraction and PO file generation functionality.
+    
+    :param argv: list of arguments passed on the command-line
+    """
+    parser = OptionParser(usage='%prog [options] dirname1 <dirname2> ...',
+                          version='%%prog %s' % VERSION)
+    parser.add_option('--charset', dest='charset', default='utf-8',
+                      help='charset to use in the output')
+    parser.add_option('-k', '--keyword', dest='keywords',
+                      default=list(KEYWORDS), action='append',
+                      help='keywords to look for in addition to the defaults. '
+                           'You can specify multiple -k flags on the command '
+                           'line.')
+    parser.add_option('--no-location', dest='no_location', default=False,
+                      action='store_true',
+                      help='do not include location comments with filename and '
+                           'line number')
+    parser.add_option('--omit-header', dest='omit_header', default=False,
+                      action='store_true',
+                      help='do not include msgid "" entry in header')
+    parser.add_option('-o', '--output', dest='output',
+                      help='path to the output POT file')
+    options, args = parser.parse_args(argv[1:])
+    if not args:
+        parser.error('incorrect number of arguments')
+
+    if options.output not in (None, '-'):
+        outfile = open(options.output, 'w')
+    else:
+        outfile = sys.stdout
+
+    try:
+        messages = []
+        for dirname in args:
+            if not os.path.isdir(dirname):
+                parser.error('%r is not a directory' % dirname)
+            extracted = extract_from_dir(dirname, keywords=options.keywords)
+            for filename, lineno, funcname, message in extracted:
+                messages.append((os.path.join(dirname, filename), lineno,
+                                 funcname, message))
+        write_po(outfile, messages,
+                 charset=options.charset, no_location=options.no_location,
+                 omit_header=options.omit_header)
+    finally:
+        if options.output:
+            outfile.close()
+
+if __name__ == '__main__':
+    main()

babel/catalog/pofile.py

+# -*- 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/.
+
+"""Reading and writing of files in the ``gettext`` PO (portable object)
+format.
+
+:see: `The Format of PO Files
+       <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_
+"""
+
+# TODO: line wrapping
+
+from datetime import datetime
+import re
+
+from babel import __version__ as VERSION
+
+__all__ = ['escape', 'normalize', 'read_po', 'write_po']
+
+POT_HEADER = """\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: %%(project)s %%(version)s\\n"
+"POT-Creation-Date: %%(time)s\\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=%%(charset)s\\n"
+"Content-Transfer-Encoding: %%(charset)s\\n"
+"Generated-By: Babel %s\\n"
+
+""" % VERSION
+
+PYTHON_FORMAT = re.compile(r'(\%\(([\w]+)\)[diouxXeEfFgGcrs])').search
+
+def escape(string):
+    r"""Escape the given string so that it can be included in double-quoted
+    strings in ``PO`` files.
+    
+    >>> escape('''Say:
+    ...   "hello, world!"
+    ... ''')
+    'Say:\\n  \\"hello, world!\\"\\n'
+    
+    :param string: the string to escape
+    :return: the escaped string
+    :rtype: `str` or `unicode`
+    """
+    return string.replace('\\', '\\\\') \
+                 .replace('\t', '\\t') \
+                 .replace('\r', '\\r') \
+                 .replace('\n', '\\n') \
+                 .replace('\"', '\\"')
+
+def normalize(string, charset='utf-8'):
+    """This converts a string into a format that is appropriate for .po files,
+    namely much closer to C style.
+    
+    :param string: the string to normalize
+    :param charset: the encoding to use for `unicode` strings
+    :return: the normalized string
+    :rtype: `str`
+    """
+    string = string.encode(charset, 'backslashreplace')
+    lines = string.split('\n')
+    if len(lines) == 1:
+        string = '"' + escape(string) + '"'
+    else:
+        if not lines[-1]:
+            del lines[-1]
+            lines[-1] = lines[-1] + '\n'
+        for i in range(len(lines)):
+            lines[i] = escape(lines[i])
+        lineterm = '\\n"\n"'
+        string = '""\n"' + lineterm.join(lines) + '"'
+    return string
+
+def read_po(fileobj):
+    """Parse a PO file.
+    
+    This function yields tuples of the form:
+    
+        ``(message, translation, locations)``
+    
+    where:
+    
+     * ``message`` is the original (untranslated) message, or a
+       ``(singular, plural)`` tuple for pluralizable messages
+     * ``translation`` is the translation of the message, or a tuple of
+       translations for pluralizable messages
+     * ``locations`` is a sequence of ``(filename, lineno)`` tuples
+    
+    :param fileobj: the file-like object to read the PO file from
+    :return: an iterator over ``(message, translation, location)`` tuples
+    :rtype: ``iterator``
+    """
+    for line in fileobj.readlines():
+        line = line.strip()
+        if line.startswith('#'):
+            continue # TODO: process comments
+        else:
+            if line.startswith('msgid_plural'):
+                msg = line[12:].lstrip()
+            elif line.startswith('msgid'):
+                msg = line[5:].lstrip()
+            elif line.startswith('msgstr'):
+                msg = line[6:].lstrip()
+                if msg.startswith('['):
+                    pass # plural
+
+def write_po(fileobj, messages, project=None, version=None, creation_date=None,
+             charset='utf-8', no_location=False, omit_header=False):
+    r"""Write a ``gettext`` PO (portable object) file to the given file-like
+    object.
+    
+    The `messages` parameter is expected to be an iterable object producing
+    tuples of the form:
+    
+        ``(filename, lineno, funcname, message)``
+    
+    >>> from StringIO import StringIO
+    >>> buf = StringIO()
+    >>> write_po(buf, [
+    ...     ('main.py', 1, None, u'foo'),
+    ...     ('main.py', 3, 'ngettext', (u'bar', u'baz'))
+    ... ], omit_header=True)
+    
+    >>> print buf.getvalue()
+    #: main.py:1
+    msgid "foo"
+    msgstr ""
+    <BLANKLINE>
+    #: main.py:3
+    msgid "bar"
+    msgid_plural "baz"
+    msgstr[0] ""
+    msgstr[1] ""
+    <BLANKLINE>
+    <BLANKLINE>
+    
+    :param fileobj: the file-like object to write to
+    :param messages: an iterable over the messages
+    :param project: the project name
+    :param version: the project version
+    :param charset: the encoding
+    :param no_location: do not emit a location comment for every message
+    :param omit_header: do not include the ``msgid ""`` entry at the top of the
+                        output
+    """
+    def _normalize(key):
+        return normalize(key, charset=charset)
+
+    if creation_date is None:
+        creation_date = datetime.now()
+
+    if not omit_header:
+        fileobj.write(POT_HEADER % {
+            'charset': charset,
+            'time': creation_date.strftime('%Y-%m-%d %H:%M'),
+            'project': project,
+            'version': version
+        })
+
+    locations = {}
+    msgids = []
+
+    for filename, lineno, funcname, key in messages:
+        if key in msgids:
+            locations[key].append((filename, lineno))
+        else:
+            locations[key] = [(filename, lineno)]
+            msgids.append(key)
+
+    for msgid in msgids:
+        if not no_location:
+            for filename, lineno in locations[msgid]:
+                fileobj.write('#: %s:%s\n' % (filename, lineno))
+        if type(msgid) is tuple:
+            assert len(msgid) == 2
+            if PYTHON_FORMAT(msgid[0]) or PYTHON_FORMAT(msgid[1]):
+                fileobj.write('#, python-format\n')
+            fileobj.write('msgid %s\n' % normalize(msgid[0], charset))
+            fileobj.write('msgid_plural %s\n' % normalize(msgid[1], charset))
+            fileobj.write('msgstr[0] ""\n')
+            fileobj.write('msgstr[1] ""\n')
+        else:
+            if PYTHON_FORMAT(msgid):
+                fileobj.write('#, python-format\n')
+            fileobj.write('msgid %s\n' % normalize(msgid, charset))
+            fileobj.write('msgstr ""\n')
+        fileobj.write('\n')

babel/catalog/tests/__init__.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import unittest
+
+def suite():
+    from babel.catalog.tests import extract, pofile
+    suite = unittest.TestSuite()
+    suite.addTest(extract.suite())
+    suite.addTest(pofile.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

babel/catalog/tests/extract.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import doctest
+import unittest
+
+from babel.catalog import extract
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(extract))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

babel/catalog/tests/pofile.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import doctest
+import unittest
+
+from babel.catalog import pofile
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(pofile))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+"""Core locale representation and locale data access gateway."""
+
+import pickle
+from pkg_resources import resource_filename
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+
+__all__ = ['Locale', 'negotiate', 'parse']
+__docformat__ = 'restructuredtext en'
+
+
+class Locale(object):
+    """Representation of a specific locale.
+    
+    >>> locale = Locale('en', territory='US')
+    >>> repr(locale)
+    '<Locale "en_US">'
+    >>> locale.display_name
+    u'English (United States)'
+    
+    A `Locale` object can also be instantiated from a raw locale string:
+    
+    >>> locale = Locale.parse('en-US', sep='-')
+    >>> repr(locale)
+    '<Locale "en_US">'
+    
+    `Locale` objects provide access to a collection of locale data, such as
+    territory and language names, number and date format patterns, and more:
+    
+    >>> locale.number_symbols['decimal']
+    u'.'
+    
+    :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_
+    """
+    _cache = {}
+    _cache_lock = threading.Lock()
+
+    def __new__(cls, language, territory=None, variant=None):
+        """Create new locale object, or load it from the cache if it had already
+        been instantiated.
+        
+        >>> l1 = Locale('en')
+        >>> l2 = Locale('en')
+        >>> l1 is l2
+        True
+        
+        :param language: the language code
+        :param territory: the territory (country or region) code
+        :param variant: the variant code
+        :return: new or existing `Locale` instance
+        :rtype: `Locale`
+        """
+        key = (language, territory, variant)
+        cls._cache_lock.acquire()
+        try:
+            self = cls._cache.get(key)
+            if self is None:
+                self = super(Locale, cls).__new__(cls, language, territory,
+                                                  variant)
+                cls._cache[key] = self
+            return self
+        finally:
+            self._cache_lock.release()
+
+    def __init__(self, language, territory=None, variant=None):
+        """Initialize the locale object from the given identifier components.
+        
+        >>> locale = Locale('en', 'US')
+        >>> locale.language
+        'en'
+        >>> locale.territory
+        'US'
+        
+        :param language: the language code
+        :param territory: the territory (country or region) code
+        :param variant: the variant code
+        """
+        self.language = language
+        self.territory = territory
+        self.variant = variant
+        self.__data = None
+
+    def parse(cls, identifier, sep='_'):
+        """Create a `Locale` instance for the given locale identifier.
+        
+        >>> l = Locale.parse('de-DE', sep='-')
+        >>> l.display_name
+        u'Deutsch (Deutschland)'
+        
+        If the `identifier` parameter is not a string, but actually a `Locale`
+        object, that object is returned:
+        
+        >>> Locale.parse(l)
+        <Locale "de_DE">
+        
+        :param identifier: the locale identifier string
+        :param sep: optional component separator
+        :return: a corresponding `Locale` instance
+        :rtype: `Locale`
+        :raise `ValueError`: if the string does not appear to be a valid locale
+                             identifier
+        """
+        if type(identifier) is cls:
+            return identifier
+        return cls(*parse(identifier, sep=sep))
+    parse = classmethod(parse)
+
+    def __repr__(self):
+        return '<Locale "%s">' % str(self)
+
+    def __str__(self):
+        return '_'.join(filter(None, [self.language, self.territory,
+                                      self.variant]))
+
+    def _data(self):
+        if self.__data is None:
+            filename = resource_filename(__name__, 'localedata/%s.dat' % self)
+            fileobj = open(filename, 'rb')
+            try:
+                self.__data = pickle.load(fileobj)
+            finally:
+                fileobj.close()
+        return self.__data
+    _data = property(_data)
+
+    def display_name(self):
+        retval = self.languages.get(self.language)
+        if self.territory:
+            variant = ''
+            if self.variant:
+                variant = ', %s' % self.variants.get(self.variant)
+            retval += ' (%s%s)' % (self.territories.get(self.territory), variant)
+        return retval
+    display_name = property(display_name, doc="""\
+        The localized display name of the locale.
+        
+        >>> Locale('en').display_name
+        u'English'
+        >>> Locale('en', 'US').display_name
+        u'English (United States)'
+        
+        :type: `unicode`
+        """)
+
+    def languages(self):
+        return self._data['languages']
+    languages = property(languages, doc="""\
+        Mapping of language codes to translated language names.
+        
+        >>> Locale('de', 'DE').languages['ja']
+        u'Japanisch'
+        
+        :type: `dict`
+        :see: `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_
+        """)
+
+    def scripts(self):
+        return self._data['scripts']
+    scripts = property(scripts, doc="""\
+        Mapping of script codes to translated script names.
+        
+        >>> Locale('en', 'US').scripts['Hira']
+        u'Hiragana'
+        
+        :type: `dict`
+        :see: `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_
+        """)
+
+    def territories(self):
+        return self._data['territories']
+    territories = property(territories, doc="""\
+        Mapping of script codes to translated script names.
+        
+        >>> Locale('es', 'CO').territories['DE']
+        u'Alemania'
+        
+        :type: `dict`
+        :see: `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_
+        """)
+
+    def variants(self):
+        return self._data['variants']
+    variants = property(variants, doc="""\
+        Mapping of script codes to translated script names.
+        
+        >>> Locale('de', 'DE').variants['1901']
+        u'alte deutsche Rechtschreibung'
+        
+        :type: `dict`
+        """)
+
+    def number_symbols(self):
+        return self._data['number_symbols']
+    number_symbols = property(number_symbols, doc="""\
+        Symbols used in number formatting.
+        
+        >>> Locale('fr', 'FR').number_symbols['decimal']
+        u','
+        
+        :type: `dict`
+        """)
+
+    def periods(self):
+        return self._data['periods']
+    periods = property(periods, doc="""\
+        Locale display names for day periods (AM/PM).
+        
+        >>> Locale('en', 'US').periods['am']
+        u'AM'
+        
+        :type: `dict`
+        """)
+
+    def days(self):
+        return self._data['days']
+    days = property(days, doc="""\
+        Locale display names for weekdays.
+        
+        >>> Locale('de', 'DE').days['format']['wide'][4]
+        u'Donnerstag'
+        
+        :type: `dict`
+        """)
+
+    def months(self):
+        return self._data['months']
+    months = property(months, doc="""\
+        Locale display names for months.
+        
+        >>> Locale('de', 'DE').months['format']['wide'][10]
+        u'Oktober'
+        
+        :type: `dict`
+        """)
+
+    def quarters(self):
+        return self._data['quarters']
+    quarters = property(quarters, doc="""\
+        Locale display names for quarters.
+        
+        >>> Locale('de', 'DE').quarters['format']['wide'][1]
+        u'1. Quartal'
+        
+        :type: `dict`
+        """)
+
+    def eras(self):
+        return self._data['eras']
+    eras = property(eras, doc="""\
+        Locale display names for eras.
+        
+        >>> Locale('en', 'US').eras['wide'][1]
+        u'Anno Domini'
+        >>> Locale('en', 'US').eras['abbreviated'][0]
+        u'BC'
+        
+        :type: `dict`
+        """)
+
+    def date_formats(self):
+        return self._data['date_formats']
+    date_formats = property(date_formats, doc="""\
+        Locale patterns for date formatting.
+        
+        >>> Locale('en', 'US').date_formats['short']
+        <DateTimeFormatPattern u'M/d/yy'>
+        >>> Locale('fr', 'FR').date_formats['long']
+        <DateTimeFormatPattern u'd MMMM yyyy'>
+        
+        :type: `dict`
+        """)
+
+    def time_formats(self):
+        return self._data['time_formats']
+    time_formats = property(time_formats, doc="""\
+        Locale patterns for time formatting.
+        
+        >>> Locale('en', 'US').time_formats['short']
+        <DateTimeFormatPattern u'h:mm a'>
+        >>> Locale('fr', 'FR').time_formats['long']
+        <DateTimeFormatPattern u'HH:mm:ss z'>
+        
+        :type: `dict`
+        """)
+
+
+def negotiate(preferred, available):
+    """Find the best match between available and requested locale strings.
+    
+    >>> negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
+    'de_DE'
+    >>> negotiate(['de_DE', 'en_US'], ['en', 'de'])
+    'de'
+    
+    :param preferred: the list of locale strings preferred by the user
+    :param available: the list of locale strings available
+    :return: the locale identifier for the best match, or `None` if no match
+             was found
+    :rtype: `str`
+    """
+    for locale in preferred:
+        if locale in available:
+            return locale
+        parts = locale.split('_')
+        if len(parts) > 1 and parts[0] in available:
+            return parts[0]
+    return None
+
+def parse(identifier, sep='_'):
+    """Parse a locale identifier into a ``(language, territory, variant)``
+    tuple.
+    
+    >>> parse('zh_CN')
+    ('zh', 'CN', None)
+    
+    The default component separator is "_", but a different separator can be
+    specified using the `sep` parameter:
+    
+    >>> parse('zh-CN', sep='-')
+    ('zh', 'CN', None)
+    
+    :param identifier: the locale identifier string
+    :param sep: character that separates the different parts of the locale
+                string
+    :return: the ``(language, territory, variant)`` tuple
+    :rtype: `tuple`
+    :raise `ValueError`: if the string does not appear to be a valid locale
+                         identifier
+    
+    :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_
+    """
+    parts = identifier.split(sep)
+    lang, territory, variant = parts[0].lower(), None, None
+    if not lang.isalpha():
+        raise ValueError('expected only letters, got %r' % lang)
+    if len(parts) > 1:
+        territory = parts[1].upper().split('.', 1)[0]
+        if not territory.isalpha():
+            raise ValueError('expected only letters, got %r' % territory)
+        if len(parts) > 2:
+            variant = parts[2].upper().split('.', 1)[0]
+    return lang, territory, variant
+# -*- 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/.
+
+"""Locale dependent formatting and parsing of dates and times.
+
+The default locale for the functions in this module is determined by the
+following environment variables, in that order:
+
+ * ``LC_TIME``,
+ * ``LC_ALL``, and
+ * ``LANG``
+"""
+
+from datetime import date, datetime, time
+
+from babel.core import Locale
+from babel.util import default_locale
+
+__all__ = ['format_date', 'format_datetime', 'format_time', 'parse_date',
+           'parse_datetime', 'parse_time']
+__docformat__ = 'restructuredtext en'
+
+LC_TIME = default_locale('LC_TIME')
+
+def get_period_names(locale=LC_TIME):
+    """Return the names for day periods (AM/PM) used by the locale.
+    
+    >>> get_period_names(locale='en_US')['am']
+    u'AM'
+    
+    :param locale: the `Locale` object, or a locale string
+    :return: the dictionary of period names
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).periods
+
+def get_day_names(width='wide', context='format', locale=LC_TIME):
+    """Return the day names used by the locale for the specified format.
+    
+    >>> get_day_names('wide', locale='en_US')[1]
+    u'Monday'
+    >>> get_day_names('abbreviated', locale='es')[1]
+    u'lun'
+    >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
+    u'M'
+    
+    :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+    :param context: the context, either "format" or "stand-alone"
+    :param locale: the `Locale` object, or a locale string
+    :return: the dictionary of day names
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).days[context][width]
+
+def get_month_names(width='wide', context='format', locale=LC_TIME):
+    """Return the month names used by the locale for the specified format.
+    
+    >>> get_month_names('wide', locale='en_US')[1]
+    u'January'
+    >>> get_month_names('abbreviated', locale='es')[1]
+    u'ene'
+    >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1]
+    u'J'
+    
+    :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+    :param context: the context, either "format" or "stand-alone"
+    :param locale: the `Locale` object, or a locale string
+    :return: the dictionary of month names
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).months[context][width]
+
+def get_quarter_names(width='wide', context='format', locale=LC_TIME):
+    """Return the quarter names used by the locale for the specified format.
+    
+    >>> get_quarter_names('wide', locale='en_US')[1]
+    u'1st quarter'
+    >>> get_quarter_names('abbreviated', locale='de_DE')[1]
+    u'Q1'
+    
+    :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+    :param context: the context, either "format" or "stand-alone"
+    :param locale: the `Locale` object, or a locale string
+    :return: the dictionary of quarter names
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).quarters[context][width]
+
+def get_era_names(width='wide', locale=LC_TIME):
+    """Return the era names used by the locale for the specified format.
+    
+    >>> get_era_names('wide', locale='en_US')[1]
+    u'Anno Domini'
+    >>> get_era_names('abbreviated', locale='de_DE')[1]
+    u'n. Chr.'
+    
+    :param width: the width to use, either "wide" or "abbreviated"
+    :param locale: the `Locale` object, or a locale string
+    :return: the dictionary of era names
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).eras[width]
+
+def get_date_format(format='medium', locale=LC_TIME):
+    """Return the date formatting patterns used by the locale for the specified
+    format.
+    
+    >>> get_date_format(locale='en_US')
+    <DateTimeFormatPattern u'MMM d, yyyy'>
+    >>> get_date_format('full', locale='de_DE')
+    <DateTimeFormatPattern u'EEEE, d. MMMM yyyy'>
+    
+    :param format: the format to use, one of "full", "long", "medium", or
+                   "short"
+    :param locale: the `Locale` object, or a locale string
+    :return: the date format pattern
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).date_formats[format]
+
+def get_time_format(format='medium', locale=LC_TIME):
+    """Return the time formatting patterns used by the locale for the specified
+    format.
+    
+    >>> get_time_format(locale='en_US')
+    <DateTimeFormatPattern u'h:mm:ss a'>
+    >>> get_time_format('full', locale='de_DE')
+    <DateTimeFormatPattern u"H:mm' Uhr 'z">
+    
+    :param format: the format to use, one of "full", "long", "medium", or
+                   "short"
+    :param locale: the `Locale` object, or a locale string
+    :return: the time format pattern
+    :rtype: `dict`
+    """
+    return Locale.parse(locale).time_formats[format]
+
+def format_date(date, format='medium', locale=LC_TIME):
+    """Returns a date formatted according to the given pattern.
+    
+    >>> d = date(2007, 04, 01)
+    >>> format_date(d, locale='en_US')
+    u'Apr 1, 2007'
+    >>> format_date(d, format='full', locale='de_DE')
+    u'Sonntag, 1. April 2007'
+    
+    :param date: the ``date`` object
+    :param format: one of "full", "long", "medium", or "short"
+    :param locale: a `Locale` object or a locale string
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if format in ('full', 'long', 'medium', 'short'):
+        format = get_date_format(format, locale=locale)
+    pattern = parse_pattern(format)
+    return parse_pattern(format).apply(date, locale)
+
+def format_datetime(datetime, format='medium', locale=LC_TIME):
+    """Returns a date formatted according to the given pattern.
+    
+    :param datetime: the ``date`` object
+    :param format: one of "full", "long", "medium", or "short"
+    :param locale: a `Locale` object or a locale string
+    :rtype: `unicode`
+    """
+    raise NotImplementedError
+
+def format_time(time, format='medium', locale=LC_TIME):
+    """Returns a time formatted according to the given pattern.
+    
+    >>> t = time(15, 30)
+    >>> format_time(t, locale='en_US')
+    u'3:30:00 PM'
+    >>> format_time(t, format='short', locale='de_DE')
+    u'15:30'
+    
+    :param time: the ``time`` object
+    :param format: one of "full", "long", "medium", or "short"
+    :param locale: a `Locale` object or a locale string
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if format in ('full', 'long', 'medium', 'short'):
+        format = get_time_format(format, locale=locale)
+    return parse_pattern(format).apply(time, locale)
+
+def parse_date(string, locale=LC_TIME):
+    raise NotImplementedError
+
+def parse_datetime(string, locale=LC_TIME):
+    raise NotImplementedError
+
+def parse_time(string, locale=LC_TIME):
+    raise NotImplementedError
+
+
+class DateTimeFormatPattern(object):
+
+    def __init__(self, pattern, format):
+        self.pattern = pattern
+        self.format = format
+
+    def __repr__(self):
+        return '<%s %r>' % (type(self).__name__, self.pattern)
+
+    def __unicode__(self):
+        return self.pattern
+
+    def __mod__(self, other):
+        assert type(other) is DateTimeFormat
+        return self.format % other
+
+    def apply(self, datetime, locale):
+        return self % DateTimeFormat(datetime, locale)
+
+
+class DateTimeFormat(object):
+
+    def __init__(self, value, locale):
+        assert isinstance(value, (date, datetime, time))
+        self.value = value
+        self.locale = Locale.parse(locale)
+
+    def __getitem__(self, name):
+        # TODO: a number of fields missing here
+        if name[0] == 'G':
+            return self.format_era(len(name))
+        elif name[0] == 'y':
+            return self.format_year(self.value.year, len(name))
+        elif name[0] == 'Y':
+            return self.format_year(self.value.isocalendar()[0], len(name))
+        elif name[0] == 'Q':
+            return self.format_quarter(len(name))
+        elif name[0] == 'q':
+            return self.format_quarter(len(name), context='stand-alone')
+        elif name[0] == 'M':
+            return self.format_month(len(name))
+        elif name[0] == 'L':
+            return self.format_month(len(name), context='stand-alone')
+        elif name[0] == 'd':
+            return self.format(self.value.day, len(name))
+        elif name[0] == 'E':
+            return self.format_weekday(len(name))
+        elif name[0] == 'c':
+            return self.format_weekday(len(name), context='stand-alone')
+        elif name[0] == 'a':
+            return self.format_period()
+        elif name[0] == 'h':
+            return self.format(self.value.hour % 12, len(name))
+        elif name[0] == 'H':
+            return self.format(self.value.hour, len(name))
+        elif name[0] == 'm':
+            return self.format(self.value.minute, len(name))
+        elif name[0] == 's':
+            return self.format(self.value.second, len(name))
+        else:
+            raise KeyError('Unsupported date/time field %r' % name[0])
+
+    def format_era(self, num):
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
+        era = int(self.value.year >= 0)
+        return get_era_names(width, self.locale)[era]
+
+    def format_year(self, value, num):
+        year = self.format(value, num)
+        if num == 2:
+            year = year[-2:]
+        return year
+
+    def format_month(self, num, context='format'):
+        if num <= 2:
+            return ('%%0%dd' % num) % self.value.month
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
+        return get_month_names(width, context, self.locale)[self.value.month]
+
+    def format_weekday(self, num, context='format'):
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
+        weekday = self.value.weekday() + 1
+        return get_day_names(width, context, self.locale)[weekday]
+
+    def format_period(self):
+        period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)]
+        return get_period_names(locale=self.locale)[period]
+
+    def format(self, value, length):
+        return ('%%0%dd' % length) % value
+
+
+PATTERN_CHARS = {
+    'G': 5,                                 # era
+    'y': None, 'Y': None, 'u': None,        # year
+    'Q': 4, 'q': 4,                         # quarter
+    'M': 5, 'L': 5,                         # month
+    'w': 2, 'W': 1,                         # week
+    'd': 2, 'D': 3, 'F': 1, 'g': None,      # day
+    'E': 5, 'e': 5, 'c': 5,                 # week day
+    'a': 1,                                 # period
+    'h': 2, 'H': 2, 'K': 2, 'k': 2,         # hour
+    'm': 2,                                 # minute
+    's': 2, 'S': None, 'A': None,           # second
+    'z': 4, 'Z': 4, 'v': 4                  # zone
+}
+
+def parse_pattern(pattern):
+    """Parse date, time, and datetime format patterns.
+    
+    >>> parse_pattern("MMMMd").format
+    u'%(MMMM)s%(d)s'
+    >>> parse_pattern("MMM d, yyyy").format
+    u'%(MMM)s %(d)s, %(yyyy)s'
+    >>> parse_pattern("H:mm' Uhr 'z").format
+    u'%(H)s:%(mm)s Uhr %(z)s'
+    
+    :param pattern: the formatting pattern to parse
+    """
+    if type(pattern) is DateTimeFormatPattern:
+        return pattern
+
+    result = []
+    quotebuf = None
+    charbuf = []
+    fieldchar = ['']
+    fieldnum = [0]
+
+    def append_chars():
+        result.append(''.join(charbuf).replace('%', '%%'))
+        del charbuf[:]
+
+    def append_field():
+        limit = PATTERN_CHARS[fieldchar[0]]
+        if limit is not None and fieldnum[0] > limit:
+            raise ValueError('Invalid length for field: %r'
+                             % (fieldchar[0] * fieldnum[0]))
+        result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0]))
+        fieldchar[0] = ''
+        fieldnum[0] = 0
+
+    for idx, char in enumerate(pattern):
+        if quotebuf is None:
+            if char == "'": # quote started
+                if fieldchar[0]:
+                    append_field()
+                elif charbuf:
+                    append_chars()
+                quotebuf = []
+            elif char in PATTERN_CHARS:
+                if charbuf:
+                    append_chars()
+                if char == fieldchar[0]:
+                    fieldnum[0] += 1
+                else:
+                    if fieldchar[0]:
+                        append_field()
+                    fieldchar[0] = char
+                    fieldnum[0] = 1
+            else:
+                if fieldchar[0]:
+                    append_field()
+                charbuf.append(char)
+
+        elif quotebuf is not None:
+            if char == "'": # quote ended
+                charbuf.extend(quotebuf)
+                quotebuf = None
+            else: # inside quote
+                quotebuf.append(char)
+
+    if fieldchar[0]:
+        append_field()
+    elif charbuf:
+        append_chars()
+
+    return DateTimeFormatPattern(pattern, u''.join(result))

babel/localedata/aa.dat

Binary file added.

babel/localedata/aa_DJ.dat

Binary file added.

babel/localedata/aa_ER.dat

Binary file added.

babel/localedata/aa_ER_SAAHO.dat

Binary file added.

babel/localedata/aa_ET.dat

Binary file added.

babel/localedata/af.dat

Binary file added.

babel/localedata/af_NA.dat

Binary file added.

babel/localedata/af_ZA.dat

Binary file added.

babel/localedata/ak.dat

Binary file added.

babel/localedata/ak_GH.dat

Binary file added.