]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Import of initial code base.
authorChristopher Lenz <cmlenz@gmail.com>
Tue, 29 May 2007 20:33:55 +0000 (20:33 +0000)
committerChristopher Lenz <cmlenz@gmail.com>
Tue, 29 May 2007 20:33:55 +0000 (20:33 +0000)
24 files changed:
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL.txt [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README.txt [new file with mode: 0644]
babel/__init__.py [new file with mode: 0644]
babel/catalog/__init__.py [new file with mode: 0644]
babel/catalog/extract.py [new file with mode: 0644]
babel/catalog/frontend.py [new file with mode: 0644]
babel/catalog/pofile.py [new file with mode: 0644]
babel/catalog/tests/__init__.py [new file with mode: 0644]
babel/catalog/tests/extract.py [new file with mode: 0644]
babel/catalog/tests/pofile.py [new file with mode: 0644]
babel/core.py [new file with mode: 0644]
babel/dates.py [new file with mode: 0644]
babel/numbers.py [new file with mode: 0644]
babel/tests/__init__.py [new file with mode: 0644]
babel/tests/core.py [new file with mode: 0644]
babel/tests/dates.py [new file with mode: 0644]
babel/tests/numbers.py [new file with mode: 0644]
babel/tests/util.py [new file with mode: 0644]
babel/util.py [new file with mode: 0644]
scripts/import_cldr.py [new file with mode: 0755]
setup.py [new file with mode: 0755]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..74202d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,28 @@
+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.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..bb5e1b7
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1 @@
+Nothing so far, not even public yet.
diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644 (file)
index 0000000..89209f8
--- /dev/null
@@ -0,0 +1,37 @@
+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
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..4fa7a34
--- /dev/null
@@ -0,0 +1,3 @@
+include babel/localedata/*.dat
+include doc/api/*.*
+include doc/*.html
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..9c55b56
--- /dev/null
@@ -0,0 +1,12 @@
+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/>
diff --git a/babel/__init__.py b/babel/__init__.py
new file mode 100644 (file)
index 0000000..cca0636
--- /dev/null
@@ -0,0 +1,32 @@
+# -*- 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
diff --git a/babel/catalog/__init__.py b/babel/catalog/__init__.py
new file mode 100644 (file)
index 0000000..3f99b35
--- /dev/null
@@ -0,0 +1,72 @@
+# -*- 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__)
diff --git a/babel/catalog/extract.py b/babel/catalog/extract.py
new file mode 100644 (file)
index 0000000..8aaa1fe
--- /dev/null
@@ -0,0 +1,214 @@
+# -*- 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
diff --git a/babel/catalog/frontend.py b/babel/catalog/frontend.py
new file mode 100644 (file)
index 0000000..31074d2
--- /dev/null
@@ -0,0 +1,150 @@
+# -*- 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()
diff --git a/babel/catalog/pofile.py b/babel/catalog/pofile.py
new file mode 100644 (file)
index 0000000..595641e
--- /dev/null
@@ -0,0 +1,206 @@
+# -*- 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')
diff --git a/babel/catalog/tests/__init__.py b/babel/catalog/tests/__init__.py
new file mode 100644 (file)
index 0000000..a371f64
--- /dev/null
@@ -0,0 +1,24 @@
+# -*- 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')
diff --git a/babel/catalog/tests/extract.py b/babel/catalog/tests/extract.py
new file mode 100644 (file)
index 0000000..8c9fcfb
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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')
diff --git a/babel/catalog/tests/pofile.py b/babel/catalog/tests/pofile.py
new file mode 100644 (file)
index 0000000..c8958e0
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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')
diff --git a/babel/core.py b/babel/core.py
new file mode 100644 (file)
index 0000000..607ea70
--- /dev/null
@@ -0,0 +1,357 @@
+# -*- 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
diff --git a/babel/dates.py b/babel/dates.py
new file mode 100644 (file)
index 0000000..4c6406d
--- /dev/null
@@ -0,0 +1,383 @@
+# -*- 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))
diff --git a/babel/numbers.py b/babel/numbers.py
new file mode 100644 (file)
index 0000000..ec9f83e
--- /dev/null
@@ -0,0 +1,164 @@
+# -*- 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 numeric data.
+
+The default locale for the functions in this module is determined by the
+following environment variables, in that order:
+
+ * ``LC_NUMERIC``,
+ * ``LC_ALL``, and
+ * ``LANG``
+"""
+# TODO: percent and scientific formatting
+
+import re
+
+from babel.core import Locale
+from babel.util import default_locale
+
+__all__ = ['format_number', 'format_decimal', 'format_currency',
+           'format_percent', 'format_scientific', 'parse_number',
+           'parse_decimal']
+__docformat__ = 'restructuredtext en'
+
+LC_NUMERIC = default_locale('LC_NUMERIC')
+
+def get_decimal_symbol(locale=LC_NUMERIC):
+    """Return the symbol used by the locale to separate decimal fractions.
+    
+    >>> get_decimal_symbol('en_US')
+    u'.'
+    
+    :param locale: the `Locale` object or locale identifier
+    :return: the decimal symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).number_symbols.get('decimal', u'.')
+
+def get_group_symbol(locale=LC_NUMERIC):
+    """Return the symbol used by the locale to separate groups of thousands.
+    
+    >>> get_group_symbol('en_US')
+    u','
+    
+    :param locale: the `Locale` object or locale identifier
+    :return: the group symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).number_symbols.get('group', u'.')
+
+def format_number(number, locale=LC_NUMERIC):
+    """Returns the given number formatted for a specific locale.
+    
+    >>> format_number(1099, locale='en_US')
+    u'1,099'
+    
+    :param number: the number to format
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted number
+    :rtype: `unicode`
+    """
+    group = get_group_symbol(locale)
+    if not group:
+        return unicode(number)
+    thou = re.compile(r'([0-9])([0-9][0-9][0-9]([%s]|$))' % group).search
+    v = str(number)
+    mo = thou(v)
+    while mo is not None:
+        l = mo.start(0)
+        v = v[:l+1] + group + v[l+1:]
+        mo = thou(v)
+    return unicode(v)
+
+def format_decimal(number, places=2, locale=LC_NUMERIC):
+    """Returns the given decimal number formatted for a specific locale.
+    
+    >>> format_decimal(1099.98, locale='en_US')
+    u'1,099.98'
+    
+    The appropriate thousands grouping and the decimal separator are used for
+    each locale:
+    
+    >>> format_decimal(1099.98, locale='de_DE')
+    u'1.099,98'
+    
+    The number of decimal places defaults to 2, but can also be specified
+    explicitly:
+    
+    >>> format_decimal(1099.98, places=4, locale='en_US')
+    u'1,099.9800'
+    
+    :param number: the number to format
+    :param places: the number of digit behind the decimal point
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted decimal number
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    a, b = (('%%.%df' % places) % number).split('.')
+    return unicode(format_number(a, locale) + get_decimal_symbol(locale) + b)
+
+def format_currency(value, locale=LC_NUMERIC):
+    """Returns formatted currency value.
+    
+    >>> format_currency(1099.98, locale='en_US')
+    u'1,099.98'
+    
+    :param value: the number to format
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted currency value
+    :rtype: `unicode`
+    """
+    return format_decimal(value, places=2, locale=locale)
+
+def format_percent(value, places=2, locale=LC_NUMERIC):
+    raise NotImplementedError
+
+def format_scientific(value, locale=LC_NUMERIC):
+    raise NotImplementedError
+
+def parse_number(string, locale=LC_NUMERIC):
+    """Parse localized number string into a long integer.
+    
+    >>> parse_number('1,099', locale='en_US')
+    1099L
+    >>> parse_number('1.099', locale='de_DE')
+    1099L
+    
+    :param string: the string to parse
+    :param locale: the `Locale` object or locale identifier
+    :return: the parsed number
+    :rtype: `long`
+    :raise `ValueError`: if the string can not be converted to a number
+    """
+    return long(string.replace(get_group_symbol(locale), ''))
+
+def parse_decimal(string, locale=LC_NUMERIC):
+    """Parse localized decimal string into a float.
+    
+    >>> parse_decimal('1,099.98', locale='en_US')
+    1099.98
+    >>> parse_decimal('1.099,98', locale='de_DE')
+    1099.98
+    
+    :param string: the string to parse
+    :param locale: the `Locale` object or locale identifier
+    :return: the parsed decimal number
+    :rtype: `float`
+    :raise `ValueError`: if the string can not be converted to a decimal number
+    """
+    locale = Locale.parse(locale)
+    string = string.replace(get_group_symbol(locale), '') \
+                   .replace(get_decimal_symbol(locale), '.')
+    return float(string)
diff --git a/babel/tests/__init__.py b/babel/tests/__init__.py
new file mode 100644 (file)
index 0000000..fa6e2cc
--- /dev/null
@@ -0,0 +1,28 @@
+# -*- 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.tests import core, dates, numbers, util
+    from babel.catalog import tests as catalog
+    suite = unittest.TestSuite()
+    suite.addTest(core.suite())
+    suite.addTest(dates.suite())
+    suite.addTest(numbers.suite())
+    suite.addTest(util.suite())
+    suite.addTest(catalog.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/babel/tests/core.py b/babel/tests/core.py
new file mode 100644 (file)
index 0000000..1337cf6
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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 import core
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(core))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/babel/tests/dates.py b/babel/tests/dates.py
new file mode 100644 (file)
index 0000000..e614d85
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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 import dates
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(dates))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/babel/tests/numbers.py b/babel/tests/numbers.py
new file mode 100644 (file)
index 0000000..68a9dc4
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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 import numbers
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(numbers))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/babel/tests/util.py b/babel/tests/util.py
new file mode 100644 (file)
index 0000000..0c780d3
--- /dev/null
@@ -0,0 +1,25 @@
+# -*- 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 import util
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(util))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/babel/util.py b/babel/util.py
new file mode 100644 (file)
index 0000000..b2b7725
--- /dev/null
@@ -0,0 +1,183 @@
+# -*- 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/.
+
+"""Various utility classes and functions."""
+
+import os
+import re
+
+__all__ = ['default_locale', 'extended_glob', 'lazy']
+__docformat__ = 'restructuredtext en'
+
+def default_locale(kind):
+    """Returns the default locale for a given category, based on environment
+    variables.
+    
+    :param kind: one of the ``LC_XXX`` environment variable names
+    :return: the value of the variable, or any of the fallbacks (``LC_ALL`` and
+             ``LANG``)
+    :rtype: `str`
+    """
+    for name in (kind, 'LC_ALL', 'LANG'):
+        locale = os.getenv(name)
+        if locale is not None:
+            return locale
+
+def extended_glob(pattern, dirname=''):
+    """Extended pathname pattern expansion.
+    
+    This function is similar to what is provided by the ``glob`` module in the
+    Python standard library, but also supports a convenience pattern ("**") to
+    match files at any directory level.
+    
+    :param pattern: the glob pattern
+    :param dirname: the path to the directory in which to search for files
+                     matching the given pattern
+    :return: an iterator over the absolute filenames of any matching files
+    :rtype: ``iterator``
+    """
+    symbols = {
+        '?':   '[^/]',
+        '?/':  '[^/]/',
+        '*':   '[^/]+',
+        '*/':  '[^/]+/',
+        '**':  '(?:.+/)*?',
+        '**/': '(?:.+/)*?',
+    }
+    buf = []
+    for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
+        if idx % 2:
+            buf.append(symbols[part])
+        elif part:
+            buf.append(re.escape(part))
+    regex = re.compile(''.join(buf) + '$')
+
+    absname = os.path.abspath(dirname)
+    for root, dirnames, filenames in os.walk(absname):
+        for subdir in dirnames:
+            if subdir.startswith('.') or subdir.startswith('_'):
+                dirnames.remove(subdir)
+        for filename in filenames:
+            filepath = relpath(
+                os.path.join(root, filename).replace(os.sep, '/'),
+                dirname
+            )
+            if regex.match(filepath):
+                yield filepath
+
+def lazy(func):
+    """Return a new function that lazily evaluates another function.
+    
+    >>> lazystr = lazy(str)
+    >>> ls = lazystr('foo')
+    >>> print ls
+    foo
+    
+    :param func: the function to wrap
+    :return: a lazily-evaluated version of the function
+    :rtype: ``function``
+    """
+    def newfunc(*args, **kwargs):
+        return LazyProxy(func, *args, **kwargs)
+    return newfunc
+
+
+class LazyProxy(object):
+    """
+    
+    >>> lazystr = LazyProxy(str, 'bar')
+    >>> print lazystr
+    bar
+    >>> u'foo' + lazystr
+    u'foobar'
+    """
+
+    def __init__(self, func, *args, **kwargs):
+        self.func = func
+        self.args = args
+        self.kwargs = kwargs
+        self._value = None
+
+    def value(self):
+        if self._value is None:
+            self._value = self.func(*self.args, **self.kwargs)
+        return self._value
+    value = property(value)
+
+    def __str__(self):
+        return str(self.value)
+
+    def __unicode__(self):
+        return unicode(self.value)
+
+    def __add__(self, other):
+        return self.value + other
+
+    def __radd__(self, other):
+        return other + self.value
+
+    def __mod__(self, other):
+        return self.value % other
+
+    def __rmod__(self, other):
+        return other % self.value
+
+    def __mul__(self, other):
+        return self.value * other
+
+    def __rmul__(self, other):
+        return other * self.value
+
+    def __call__(self, *args, **kwargs):
+        return self.value(*args, **kwargs)
+
+    def __cmp__(self, other):
+        return cmp(self.value, other)
+
+    def __rcmp__(self, other):
+        return other + self.value
+
+    def __eq__(self, other):
+        return self.value == other
+
+#    def __delattr__(self, name):
+#        delattr(self.value, name)
+#
+#    def __getattr__(self, name):
+#        return getattr(self.value, name)
+#
+#    def __setattr__(self, name, value):
+#        setattr(self.value, name, value)
+
+    def __delitem__(self, key):
+        del self.value[name]
+
+    def __getitem__(self, key):
+        return self.value[name]
+
+    def __setitem__(self, key, value):
+        self.value[name] = value
+
+
+try:
+    relpath = os.path.relpath
+except AttributeError:
+    def relpath(path, start='.'):
+        start_list = os.path.abspath(start).split(os.sep)
+        path_list = os.path.abspath(path).split(os.sep)
+
+        # Work out how much of the filepath is shared by start and path.
+        i = len(os.path.commonprefix([start_list, path_list]))
+
+        rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:]
+        return os.path.join(*rel_list)
diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py
new file mode 100755 (executable)
index 0000000..a13de62
--- /dev/null
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# -*- 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/.
+
+import copy
+from optparse import OptionParser
+import os
+import pickle
+import sys
+try:
+    from xml.etree.ElementTree import parse
+except ImportError:
+    from elementtree.ElementTree import parse
+
+from babel.dates import parse_pattern
+
+def _parent(locale):
+    parts = locale.split('_')
+    if len(parts) == 1:
+        return 'root'
+    else:
+        return '_'.join(parts[:-1])
+
+def _text(elem):
+    buf = [elem.text or '']
+    for child in elem:
+        buf.append(_text(child))
+    buf.append(elem.tail or '')
+    return u''.join(filter(None, buf)).strip()
+
+def main():
+    parser = OptionParser(usage='%prog path/to/cldr')
+    options, args = parser.parse_args()
+    if len(args) != 1:
+        parser.error('incorrect number of arguments')
+
+    srcdir = args[0]
+    destdir = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])),
+                           '..', 'babel', 'localedata')
+
+    filenames = os.listdir(os.path.join(srcdir, 'main'))
+    filenames.remove('root.xml')
+    filenames.sort(lambda a,b: len(a)-len(b))
+    filenames.insert(0, 'root.xml')
+
+    dicts = {}
+
+    for filename in filenames:
+        print>>sys.stderr, 'Processing input file %r' % filename
+        stem, ext = os.path.splitext(filename)
+        if ext != '.xml':
+            continue
+
+        data = {}
+        if stem != 'root':
+            data.update(copy.deepcopy(dicts[_parent(stem)]))
+        tree = parse(os.path.join(srcdir, 'main', filename))
+
+        # <localeDisplayNames>
+
+        territories = data.setdefault('territories', {})
+        for elem in tree.findall('//territories/territory'):
+            if 'draft' in elem.attrib and elem.attrib['type'] in territories:
+                continue
+            territories[elem.attrib['type']] = _text(elem)
+
+        languages = data.setdefault('languages', {})
+        for elem in tree.findall('//languages/language'):
+            if 'draft' in elem.attrib and elem.attrib['type'] in languages:
+                continue
+            languages[elem.attrib['type']] = _text(elem)
+
+        variants = data.setdefault('variants', {})
+        for elem in tree.findall('//variants/variant'):
+            if 'draft' in elem.attrib and elem.attrib['type'] in variants:
+                continue
+            variants[elem.attrib['type']] = _text(elem)
+
+        scripts = data.setdefault('scripts', {})
+        for elem in tree.findall('//scripts/script'):
+            if 'draft' in elem.attrib and elem.attrib['type'] in scripts:
+                continue
+            scripts[elem.attrib['type']] = _text(elem)
+
+        # <dates>
+
+        time_zones = data.setdefault('time_zones', {})
+        for elem in tree.findall('//timeZoneNames/zone'):
+            time_zones[elem.tag] = unicode(elem.findtext('displayName'))
+
+        for calendar in tree.findall('//calendars/calendar'):
+            if calendar.attrib['type'] != 'gregorian':
+                # TODO: support other calendar types
+                continue
+
+            months = data.setdefault('months', {})
+            for ctxt in calendar.findall('months/monthContext'):
+                ctxts = months.setdefault(ctxt.attrib['type'], {})
+                for width in ctxt.findall('monthWidth'):
+                    widths = ctxts.setdefault(width.attrib['type'], {})
+                    for elem in width.findall('month'):
+                        if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+                            continue
+                        widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+            days = data.setdefault('days', {})
+            for ctxt in calendar.findall('days/dayContext'):
+                ctxts = days.setdefault(ctxt.attrib['type'], {})
+                for width in ctxt.findall('dayWidth'):
+                    widths = ctxts.setdefault(width.attrib['type'], {})
+                    for elem in width.findall('day'):
+                        dtype = {'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4,
+                                 'fri': 5, 'sat': 6, 'sun': 7}[elem.attrib['type']]
+                        if 'draft' in elem.attrib and dtype in widths:
+                            continue
+                        widths[dtype] = unicode(elem.text)
+
+            quarters = data.setdefault('quarters', {})
+            for ctxt in calendar.findall('quarters/quarterContext'):
+                ctxts = quarters.setdefault(ctxt.attrib['type'], {})
+                for width in ctxt.findall('quarterWidth'):
+                    widths = ctxts.setdefault(width.attrib['type'], {})
+                    for elem in width.findall('quarter'):
+                        if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+                            continue
+                        widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+            eras = data.setdefault('eras', {})
+            for width in calendar.findall('eras/*'):
+                ewidth = {'eraNames': 'wide', 'eraAbbr': 'abbreviated'}[width.tag]
+                widths = eras.setdefault(ewidth, {})
+                for elem in width.findall('era'):
+                    if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+                        continue
+                    widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+            # AM/PM
+            periods = data.setdefault('periods', {})
+            for elem in calendar.findall('am'):
+                if 'draft' in elem.attrib and elem.tag in periods:
+                    continue
+                periods[elem.tag] = unicode(elem.text)
+            for elem in calendar.findall('pm'):
+                if 'draft' in elem.attrib and elem.tag in periods:
+                    continue
+                periods[elem.tag] = unicode(elem.text)
+
+            date_formats = data.setdefault('date_formats', {})
+            for elem in calendar.findall('dateFormats/dateFormatLength'):
+                if 'draft' in elem.attrib and elem.attrib.get('type') in date_formats:
+                    continue
+                try:
+                    date_formats[elem.attrib.get('type')] = \
+                        parse_pattern(unicode(elem.findtext('dateFormat/pattern')))
+                except ValueError, e:
+                    print e
+
+            time_formats = data.setdefault('time_formats', {})
+            for elem in calendar.findall('timeFormats/timeFormatLength'):
+                if 'draft' in elem.attrib and elem.attrib.get('type') in time_formats:
+                    continue
+                try:
+                    time_formats[elem.attrib.get('type')] = \
+                        parse_pattern(unicode(elem.findtext('timeFormat/pattern')))
+                except ValueError, e:
+                    print e
+
+        # <numbers>
+
+        number_symbols = data.setdefault('number_symbols', {})
+        for elem in tree.findall('//numbers/symbols/*'):
+            number_symbols[elem.tag] = unicode(elem.text)
+
+        decimal_formats = data.setdefault('decimal_formats', {})
+        for elem in tree.findall('//decimalFormats/decimalFormatLength'):
+            if 'draft' in elem.attrib and elem.attrib.get('type') in decimal_formats:
+                continue
+            decimal_formats[elem.attrib.get('type')] = unicode(elem.findtext('decimalFormat/pattern'))
+
+        scientific_formats = data.setdefault('scientific_formats', {})
+        for elem in tree.findall('//scientificFormats/scientificFormatLength'):
+            if 'draft' in elem.attrib and elem.attrib.get('type') in scientific_formats:
+                continue
+            scientific_formats[elem.attrib.get('type')] = unicode(elem.findtext('scientificFormat/pattern'))
+
+        currency_formats = data.setdefault('currency_formats', {})
+        for elem in tree.findall('//currencyFormats/currencyFormatLength'):
+            if 'draft' in elem.attrib and elem.attrib.get('type') in currency_formats:
+                continue
+            currency_formats[elem.attrib.get('type')] = unicode(elem.findtext('currencyFormat/pattern'))
+
+        percent_formats = data.setdefault('percent_formats', {})
+        for elem in tree.findall('//percentFormats/percentFormatLength'):
+            if 'draft' in elem.attrib and elem.attrib.get('type') in percent_formats:
+                continue
+            percent_formats[elem.attrib.get('type')] = unicode(elem.findtext('percentFormat/pattern'))
+
+        currencies = data.setdefault('currencies', {})
+        for elem in tree.findall('//currencies/currency'):
+            currencies[elem.attrib['type']] = {
+                'display_name': unicode(elem.findtext('displayName')),
+                'symbol': unicode(elem.findtext('symbol'))
+            }
+
+        dicts[stem] = data
+        outfile = open(os.path.join(destdir, stem + '.dat'), 'wb')
+        try:
+            pickle.dump(data, outfile, 2)
+        finally:
+            outfile.close()
+
+if __name__ == '__main__':
+    main()
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..8ad82af
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- 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/.
+
+import doctest
+from glob import glob
+import os
+from setuptools import find_packages, setup, Command
+import sys
+
+
+class build_doc(Command):
+    description = 'Builds the documentation'
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        from docutils.core import publish_cmdline
+        docutils_conf = os.path.join('doc', 'docutils.conf')
+        epydoc_conf = os.path.join('doc', 'epydoc.conf')
+
+        for source in glob('doc/*.txt'):
+            dest = os.path.splitext(source)[0] + '.html'
+            if not os.path.exists(dest) or \
+                   os.path.getmtime(dest) < os.path.getmtime(source):
+                print 'building documentation file %s' % dest
+                publish_cmdline(writer_name='html',
+                                argv=['--config=%s' % docutils_conf, source,
+                                      dest])
+
+        try:
+            from epydoc import cli
+            old_argv = sys.argv[1:]
+            sys.argv[1:] = [
+                '--config=%s' % epydoc_conf,
+                '--no-private', # epydoc bug, not read from config
+                '--simple-term',
+                '--verbose'
+            ]
+            cli.cli()
+            sys.argv[1:] = old_argv
+
+        except ImportError:
+            print 'epydoc not installed, skipping API documentation.'
+
+
+class test_doc(Command):
+    description = 'Tests the code examples in the documentation'
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        for filename in glob('doc/*.txt'):
+            print 'testing documentation file %s' % filename
+            doctest.testfile(filename, False, optionflags=doctest.ELLIPSIS)
+
+
+setup(
+    name = 'Babel',
+    version = '0.1',
+    description = 'Internationalization utilities',
+    long_description = \
+"""A collection of tools for internationalizing Python applications.""",
+    author = 'Edgewall Software',
+    author_email = 'info@edgewall.org',
+    license = 'BSD',
+    url = 'http://babel.edgewall.org/',
+    download_url = 'http://babel.edgewall.org/wiki/Download',
+    zip_safe = False,
+
+    classifiers = [
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+    packages = find_packages(exclude=['tests']),
+    package_data = {'babel': ['localedata/*.dat']},
+    test_suite = 'babel.tests.suite',
+
+    entry_points = """
+    [console_scripts]
+    pygettext = babel.catalog.frontend:main
+    
+    [distutils.commands]
+    extract_messages = babel.catalog.frontend:extract_messages
+    
+    [babel.extractors]
+    genshi = babel.catalog.extract:extract_genshi
+    python = babel.catalog.extract:extract_python
+    """,
+
+    cmdclass = {'build_doc': build_doc, 'test_doc': test_doc}
+)